Index: /branches/eam_branch_20071023/psModules/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/.cvsignore	(revision 15358)
@@ -0,0 +1,29 @@
+autom4te.cache
+Makefile
+config.log
+config.status
+libtool
+Makefile.in
+aclocal.m4
+configure
+Doxyfile
+DoxygenLog
+docs
+man
+psmodules-config
+psmodules.pc
+bin
+include
+lib
+config.guess
+config.sub
+depcomp
+install-sh
+ltmain.sh
+missing
+psmodule.kdevelop.pcs
+psmodule-*.tar.gz
+psmodule-*.tar.bz2
+compile
+ChangeLog
+psmodules-*.tar.*
Index: /branches/eam_branch_20071023/psModules/AUTHORS
===================================================================
--- /branches/eam_branch_20071023/psModules/AUTHORS	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/AUTHORS	(revision 15358)
@@ -0,0 +1,1 @@
+Maui High Performance Computing Center, University of Hawai'i
Index: /branches/eam_branch_20071023/psModules/COPYING
===================================================================
--- /branches/eam_branch_20071023/psModules/COPYING	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/COPYING	(revision 15358)
@@ -0,0 +1,6 @@
+(C) 2004 University of Hawai'i
+
+Permission to copy/distribute to be governed by:
+    Institute for Astronomy
+    2680 Woodlawn Drive
+    Honolulu, HI 96822-1897
Index: /branches/eam_branch_20071023/psModules/Doxyfile.in
===================================================================
--- /branches/eam_branch_20071023/psModules/Doxyfile.in	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/Doxyfile.in	(revision 15358)
@@ -0,0 +1,1079 @@
+# Doxyfile 1.3.4
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project
+#
+# All text after a hash (#) is considered a comment and will be ignored
+# The format is:
+#       TAG = value [value, ...]
+# For lists items can also be appended using:
+#       TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (" ")
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded
+# by quotes) that should identify the project.
+
+PROJECT_NAME           = "Pan-STARRS Module Library"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number.
+# This could be handy for archiving the generated documentation or
+# if some version control system is used.
+
+PROJECT_NUMBER         = @VERSION@
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
+# base path where the generated documentation will be put.
+# If a relative path is entered, it will be relative to the location
+# where doxygen was started. If left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = @builddir@/docs
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# The default language is English, other supported languages are:
+# Brazilian, Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch,
+# Finnish, French, German, Greek, Hungarian, Italian, Japanese, Japanese-en
+# (Japanese with English messages), Korean, Norwegian, Polish, Portuguese,
+# Romanian, Russian, Serbian, Slovak, Slovene, Spanish, Swedish, and Ukrainian.
+
+OUTPUT_LANGUAGE        = English
+
+# This tag can be used to specify the encoding used in the generated output.
+# The encoding is not always determined by the language that is chosen,
+# but also whether or not the output is meant for Windows or non-Windows users.
+# In case there is a difference, setting the USE_WINDOWS_ENCODING tag to YES
+# forces the Windows encoding (this is the default for the Windows binary),
+# whereas setting the tag to NO uses a Unix-style encoding (the default for
+# all platforms other than Windows).
+
+USE_WINDOWS_ENCODING   = NO
+
+# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will
+# include brief member descriptions after the members that are listed in
+# the file and class documentation (similar to JavaDoc).
+# Set to NO to disable this.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend
+# the brief description of a member or function before the detailed description.
+# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+
+REPEAT_BRIEF           = YES
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# Doxygen will generate a detailed section even if there is only a brief
+# description.
+
+ALWAYS_DETAILED_SEC    = YES
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all inherited
+# members of a class in the documentation of that class as if those members were
+# ordinary class members. Constructors, destructors and assignment operators of
+# the base classes will not be shown.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full
+# path before files name in the file list and in the header files. If set
+# to NO the shortest path that makes the file name unique will be used.
+
+FULL_PATH_NAMES        = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag
+# can be used to strip a user-defined part of the path. Stripping is
+# only done if one of the specified strings matches the left-hand part of
+# the path. It is allowed to use relative paths in the argument list.
+
+STRIP_FROM_PATH        =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter
+# (but less readable) file names. This can be useful is your file systems
+# doesn't support long names like on DOS, Mac, or CD-ROM.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen
+# will interpret the first line (until the first dot) of a JavaDoc-style
+# comment as the brief description. If set to NO, the JavaDoc
+# comments will behave just like the Qt-style comments (thus requiring an
+# explict @brief command for a brief description.
+
+JAVADOC_AUTOBRIEF      = YES
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen
+# treat a multi-line C++ special comment block (i.e. a block of //! or ///
+# comments) as a brief description. This used to be the default behaviour.
+# The new default is to treat a multi-line C++ comment block as a detailed
+# description. Set this tag to YES if you prefer the old behaviour instead.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the DETAILS_AT_TOP tag is set to YES then Doxygen
+# will output the detailed description near the top, like JavaDoc.
+# If set to NO, the detailed description appears after the member
+# documentation.
+
+DETAILS_AT_TOP         = YES
+
+# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented
+# member inherits the documentation from any documented member that it
+# reimplements.
+
+INHERIT_DOCS           = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES, then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab.
+# Doxygen uses this value to replace tabs by spaces in code fragments.
+
+TAB_SIZE               = 4
+
+# This tag can be used to specify a number of aliases that acts
+# as commands in the documentation. An alias has the form "name=value".
+# For example adding "sideeffect=\par Side Effects:\n" will allow you to
+# put the command \sideeffect (or @sideeffect) in the documentation, which
+# will result in a user-defined paragraph with heading "Side Effects:".
+# You can put \n's in the value part of an alias to insert newlines.
+
+ALIASES                =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C.
+# For instance, some of the names that are used will be different. The list
+# of all members will be omitted, etc.
+
+OPTIMIZE_OUTPUT_FOR_C  = YES
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java sources
+# only. Doxygen will then generate output that is more tailored for Java.
+# For instance, namespaces will be presented as packages, qualified scopes
+# will look different, etc.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the SUBGROUPING tag to YES (the default) to allow class member groups of
+# the same type (for instance a group of public functions) to be put as a
+# subgroup of that type (e.g. under the Public Functions section). Set it to
+# NO to prevent subgrouping. Alternatively, this can be done per class using
+# the \nosubgrouping command.
+
+SUBGROUPING            = NO
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
+# documentation are documented, even if no documentation was available.
+# Private class members and static file members will be hidden unless
+# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES
+
+EXTRACT_ALL            = YES
+
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class
+# will be included in the documentation.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES all static members of a file
+# will be included in the documentation.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs)
+# defined locally in source files will be included in the documentation.
+# If set to NO only classes defined in header files are included.
+
+EXTRACT_LOCAL_CLASSES  = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all
+# undocumented members of documented classes, files or namespaces.
+# If set to NO (the default) these members will be included in the
+# various overviews, but no documentation section is generated.
+# This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy.
+# If set to NO (the default) these classes will be included in the various
+# overviews. This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all
+# friend (class|struct|union) declarations.
+# If set to NO (the default) these declarations will be included in the
+# documentation.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any 
+# documentation blocks found inside the body of a function. 
+# If set to NO (the default) these blocks will be appended to the 
+# function's detailed documentation block.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation 
+# that is typed after a \internal command is included. If the tag is set 
+# to NO (the default) then the documentation will be excluded.
+# Set it to YES to include the internal documentation.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate 
+# file names in lower-case letters. If set to YES upper-case letters are also 
+# allowed. This is useful if you have classes or files whose names only differ 
+# in case and if your file system supports case sensitive file names. Windows 
+# users are advised to set this option to NO.
+
+CASE_SENSE_NAMES       = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen 
+# will show members with their full class and namespace scopes in the 
+# documentation. If set to YES the scope will be hidden.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen 
+# will put a list of the files that are included by a file in the documentation 
+# of that file.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] 
+# is inserted in the documentation for inline members.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen 
+# will sort the (detailed) documentation of file and class members 
+# alphabetically by member name. If set to NO the members will appear in 
+# declaration order.
+
+SORT_MEMBER_DOCS       = YES
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or 
+# disable (NO) the todo list. This list is created by putting \todo 
+# commands in the documentation.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or 
+# disable (NO) the test list. This list is created by putting \test 
+# commands in the documentation.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or 
+# disable (NO) the bug list. This list is created by putting \bug 
+# commands in the documentation.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or 
+# disable (NO) the deprecated list. This list is created by putting 
+# \deprecated commands in the documentation.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional 
+# documentation sections, marked by \if sectionname ... \endif.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines 
+# the initial value of a variable or define consists of for it to appear in 
+# the documentation. If the initializer consists of more lines than specified
+# here it will be hidden. Use a value of 0 to hide initializers completely. 
+# The appearance of the initializer of individual variables and defines in the 
+# documentation can be controlled using \showinitializer or \hideinitializer 
+# command in the documentation regardless of this setting.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated 
+# at the bottom of the documentation of classes and structs. If set to YES the 
+# list will mention the files that were used to generate the documentation.
+
+SHOW_USED_FILES        = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated 
+# by doxygen. Possible values are YES and NO. If left blank NO is used.
+
+QUIET                  = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are 
+# generated by doxygen. Possible values are YES and NO. If left blank
+# NO is used.
+
+WARNINGS               = YES
+
+# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings 
+# for undocumented members. If EXTRACT_ALL is set to YES then this flag will 
+# automatically be disabled.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for 
+# potential errors in the documentation, such as not documenting some 
+# parameters in a documented function, or documenting parameters that 
+# don't exist or using markup commands wrongly.
+
+WARN_IF_DOC_ERROR      = YES
+
+# The WARN_FORMAT tag determines the format of the warning messages that 
+# doxygen can produce. The string should contain the $file, $line, and $text 
+# tags, which will be replaced by the file and line number from which the 
+# warning originated and the warning text.
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning 
+# and error messages should be written. If left blank the output is written 
+# to stderr.
+
+WARN_LOGFILE           = DoxygenLog
+
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag can be used to specify the files and/or directories that contain 
+# documented source files. You may enter file names like "myfile.cpp" or 
+# directories like "/usr/src/myproject". Separate the files or directories 
+# with spaces.
+
+INPUT                  = src
+
+# If the value of the INPUT tag contains directories, you can use the 
+# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank the following patterns are tested:
+# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx *.hpp
+# *.h++ *.idl *.odl *.cs *.php *.php3 *.inc
+
+FILE_PATTERNS          = *.h *.dox
+
+# The RECURSIVE tag can be used to turn specify whether or not subdirectories
+# should be searched for input files as well. Possible values are YES and NO.
+# If left blank NO is used.
+
+RECURSIVE              = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should
+# excluded from the INPUT source files. This way you can easily exclude a 
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used select whether or not files or directories 
+# that are symbolic links (a Unix filesystem feature) are excluded from the input.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the 
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude 
+# certain files from those directories.
+
+EXCLUDE_PATTERNS       = *_wrap.c
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or
+# directories that contain example code fragments that are included (see 
+# the \include command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the 
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp 
+# and *.h) to filter out the source-files in the directories. If left 
+# blank all files are included.
+
+EXAMPLE_PATTERNS       = 
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be 
+# searched for input files to be used with the \include or \dontinclude 
+# commands irrespective of the value of the RECURSIVE tag. 
+# Possible values are YES and NO. If left blank NO is used.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or 
+# directories that contain image that are included in the documentation (see 
+# the \image command).
+
+IMAGE_PATH             = 
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should 
+# invoke to filter for each input file. Doxygen will invoke the filter program 
+# by executing (via popen()) the command <filter> <input-file>, where <filter> 
+# is the value of the INPUT_FILTER tag, and <input-file> is the name of an 
+# input file. Doxygen will then use the output that the filter program writes 
+# to standard output.
+
+INPUT_FILTER           = 
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using 
+# INPUT_FILTER) will be used to filter the input files when producing source 
+# files to browse (i.e. when SOURCE_BROWSER is set to YES).
+
+FILTER_SOURCE_FILES    = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will 
+# be generated. Documented entities will be cross-referenced with these sources.
+
+SOURCE_BROWSER         = YES
+
+# Setting the INLINE_SOURCES tag to YES will include the body 
+# of functions and classes directly in the documentation.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct 
+# doxygen to hide any special comment blocks from generated source code 
+# fragments. Normal C and C++ comments will always remain visible.
+
+STRIP_CODE_COMMENTS    = NO
+
+# If the REFERENCED_BY_RELATION tag is set to YES (the default) 
+# then for each documented function all documented 
+# functions referencing it will be listed.
+
+REFERENCED_BY_RELATION = YES
+
+# If the REFERENCES_RELATION tag is set to YES (the default) 
+# then for each documented function all documented entities 
+# called/used by that function will be listed.
+
+REFERENCES_RELATION    = YES
+
+# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen 
+# will generate a verbatim copy of the header file for each class for 
+# which an include is specified. Set to NO to disable this.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index 
+# of all compounds will be generated. Enable this if the project 
+# contains a lot of classes, structs, unions or interfaces.
+
+ALPHABETICAL_INDEX     = YES
+
+# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then 
+# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns 
+# in which this list will be split (can be a number in the range [1..20])
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all 
+# classes will be put under the same header in the alphabetical index. 
+# The IGNORE_PREFIX tag can be used to specify one or more prefixes that 
+# should be ignored while generating the index headers.
+
+IGNORE_PREFIX          = 
+
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES (the default) Doxygen will 
+# generate HTML output.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `html' will be used as the default path.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for 
+# each generated HTML page (for example: .htm,.php,.asp). If it is left blank 
+# doxygen will generate files with .html extension.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a personal HTML header for 
+# each generated HTML page. If it is left blank doxygen will generate a 
+# standard header.
+
+HTML_HEADER            = 
+
+# The HTML_FOOTER tag can be used to specify a personal HTML footer for 
+# each generated HTML page. If it is left blank doxygen will generate a 
+# standard footer.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading 
+# style sheet that is used by each HTML page. It can be used to 
+# fine-tune the look of the HTML output. If the tag is left blank doxygen 
+# will generate a default style sheet
+
+HTML_STYLESHEET        = 
+
+# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, 
+# files or namespaces will be aligned in HTML using tables. If set to 
+# NO a bullet list will be used.
+
+HTML_ALIGN_MEMBERS     = YES
+
+# If the GENERATE_HTMLHELP tag is set to YES, additional index files 
+# will be generated that can be used as input for tools like the 
+# Microsoft HTML help workshop to generate a compressed HTML help file (.chm) 
+# of the generated HTML documentation.
+
+GENERATE_HTMLHELP      = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can 
+# be used to specify the file name of the resulting .chm file. You 
+# can add a path in front of the file if the result should not be 
+# written to the html output dir.
+
+CHM_FILE               = 
+
+# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can 
+# be used to specify the location (absolute path including file name) of 
+# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run 
+# the HTML help compiler on the generated index.hhp.
+
+HHC_LOCATION           = 
+
+# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag 
+# controls if a separate .chi index file is generated (YES) or that 
+# it should be included in the master .chm file (NO).
+
+GENERATE_CHI           = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag 
+# controls whether a binary table of contents is generated (YES) or a 
+# normal table of contents (NO) in the .chm file.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members 
+# to the contents of the HTML help documentation and to the tree view.
+
+TOC_EXPAND             = YES
+
+# The DISABLE_INDEX tag can be used to turn on/off the condensed index at 
+# top of each HTML page. The value NO (the default) enables the index and 
+# the value YES disables it.
+
+DISABLE_INDEX          = NO
+
+# This tag can be used to set the number of enum values (range [1..20]) 
+# that doxygen will group on one line in the generated HTML documentation.
+
+ENUM_VALUES_PER_LINE   = 10
+
+# If the GENERATE_TREEVIEW tag is set to YES, a side panel will be
+# generated containing a tree-like index structure (just like the one that 
+# is generated for HTML Help). For this to work a browser that supports 
+# JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+, 
+# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are 
+# probably better off using the HTML help feature.
+
+GENERATE_TREEVIEW      = NO
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be 
+# used to set the initial width (in pixels) of the frame in which the tree 
+# is shown.
+
+TREEVIEW_WIDTH         = 250
+
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will 
+# generate Latex output.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `latex' will be used as the default path.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be 
+# invoked. If left blank `latex' will be used as the default command name.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to 
+# generate index for LaTeX. If left blank `makeindex' will be used as the 
+# default command name.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact 
+# LaTeX documents. This may be useful for small projects and may help to 
+# save some trees in general.
+
+COMPACT_LATEX          = YES
+
+# The PAPER_TYPE tag can be used to set the paper type that is used 
+# by the printer. Possible values are: a4, a4wide, letter, legal and 
+# executive. If left blank a4wide will be used.
+
+PAPER_TYPE             = letter
+
+# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX 
+# packages that should be included in the LaTeX output.
+
+EXTRA_PACKAGES         = times
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for 
+# the generated latex document. The header should contain everything until 
+# the first chapter. If it is left blank doxygen will generate a 
+# standard header. Notice: only use this tag if you know what you are doing!
+
+LATEX_HEADER           = 
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated 
+# is prepared for conversion to pdf (using ps2pdf). The pdf file will 
+# contain links (just like the HTML output) instead of page references 
+# This makes the output suitable for online browsing using a pdf viewer.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of 
+# plain latex in the generated Makefile. Set this option to YES to get a 
+# higher quality PDF documentation.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. 
+# command to the generated LaTeX files. This will instruct LaTeX to keep 
+# running if errors occur, instead of asking the user for help. 
+# This option is also used when generating formulas in HTML.
+
+LATEX_BATCHMODE        = NO
+
+# If LATEX_HIDE_INDICES is set to YES then doxygen will not 
+# include the index chapters (such as File Index, Compound Index, etc.) 
+# in the output.
+
+LATEX_HIDE_INDICES     = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output 
+# The RTF output is optimised for Word 97 and may not look very pretty with 
+# other RTF readers or editors.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `rtf' will be used as the default path.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES Doxygen generates more compact 
+# RTF documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated 
+# will contain hyperlink fields. The RTF file will 
+# contain links (just like the HTML output) instead of page references. 
+# This makes the output suitable for online browsing using WORD or other 
+# programs which support those fields. 
+# Note: wordpad (write) and others do not support links.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's 
+# config file, i.e. a series of assigments. You only have to provide 
+# replacements, missing definitions are set to their default value.
+
+RTF_STYLESHEET_FILE    = 
+
+# Set optional variables used in the generation of an rtf document. 
+# Syntax is similar to doxygen's config file.
+
+RTF_EXTENSIONS_FILE    = 
+
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES (the default) Doxygen will 
+# generate man pages
+
+GENERATE_MAN           = YES
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `man' will be used as the default path.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to
+# the generated man pages (default is the subroutine's section .3)
+
+MAN_EXTENSION          = .3
+
+# If the MAN_LINKS tag is set to YES and Doxygen generates man output, 
+# then it will generate one additional man file for each entity 
+# documented in the real man page(s). These additional files 
+# only source the real man page, but without them the man command 
+# would be unable to find the correct page. The default is NO.
+
+MAN_LINKS              = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES Doxygen will 
+# generate an XML file that captures the structure of 
+# the code including all documentation. Note that this 
+# feature is still experimental and incomplete at the 
+# moment.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `xml' will be used as the default path.
+
+XML_OUTPUT             = xml
+
+# The XML_SCHEMA tag can be used to specify an XML schema, 
+# which can be used by a validating XML parser to check the 
+# syntax of the XML files.
+
+XML_SCHEMA             = 
+
+# The XML_DTD tag can be used to specify an XML DTD, 
+# which can be used by a validating XML parser to check the 
+# syntax of the XML files.
+
+XML_DTD                = 
+
+#---------------------------------------------------------------------------
+# configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will 
+# generate an AutoGen Definitions (see autogen.sf.net) file 
+# that captures the structure of the code including all 
+# documentation. Note that this feature is still experimental 
+# and incomplete at the moment.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES Doxygen will 
+# generate a Perl module file that captures the structure of 
+# the code including all documentation. Note that this 
+# feature is still experimental and incomplete at the 
+# moment.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES Doxygen will generate 
+# the necessary Makefile rules, Perl scripts and LaTeX code to be able 
+# to generate PDF and DVI output from the Perl module output.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be 
+# nicely formatted so it can be parsed by a human reader.  This is useful 
+# if you want to understand what is going on.  On the other hand, if this 
+# tag is set to NO the size of the Perl module output will be much smaller 
+# and Perl will parse it just the same.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file 
+# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. 
+# This is useful so different doxyrules.make files included by the same 
+# Makefile don't overwrite each other's variables.
+
+PERLMOD_MAKEVAR_PREFIX = 
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will
+# evaluate all C-preprocessor directives found in the sources and include
+# files.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro 
+# names in the source code. If set to NO (the default) only conditional 
+# compilation will be performed. Macro expansion can be done in a controlled 
+# way by setting EXPAND_ONLY_PREDEF to YES.
+
+MACRO_EXPANSION        = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES 
+# then the macro expansion is limited to the macros specified with the 
+# PREDEFINED and EXPAND_AS_PREDEFINED tags.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files 
+# in the INCLUDE_PATH (see below) will be search if a #include is found.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that 
+# contain include files that are not input files but should be processed by 
+# the preprocessor.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard 
+# patterns (like *.h and *.hpp) to filter out the header-files in the 
+# directories. If left blank, the patterns specified with FILE_PATTERNS will 
+# be used.
+
+INCLUDE_FILE_PATTERNS  = 
+
+# The PREDEFINED tag can be used to specify one or more macro names that 
+# are defined before the preprocessor is started (similar to the -D option of
+# gcc). The argument of the tag is a list of macros of the form: name
+# or name=definition (no spaces). If the definition and the = are
+# omitted =1 is assumed.
+
+PREDEFINED             = DOXYGEN
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then
+# this tag can be used to specify a list of macro names that should be expanded.
+# The macro definition that is found in the sources will be used. 
+# Use the PREDEFINED tag if you want to use a different macro definition.
+
+EXPAND_AS_DEFINED      = 
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then 
+# doxygen's preprocessor will remove all function-like macros that are alone 
+# on a line, have an all uppercase name, and do not end with a semicolon. Such 
+# function macros are typically used for boiler-plate code, and will confuse the 
+# parser if not removed.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration::addtions related to external references   
+#---------------------------------------------------------------------------
+
+# The TAGFILES option can be used to specify one or more tagfiles. 
+# Optionally an initial location of the external documentation 
+# can be added for each tagfile. The format of a tag file without 
+# this location is as follows: 
+#   TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows: 
+#   TAGFILES = file1=loc1 "file2 = loc2" ... 
+# where "loc1" and "loc2" can be relative or absolute paths or 
+# URLs. If a location is present for each tag, the installdox tool 
+# does not have to be run to correct the links.
+# Note that each tag file must have a unique name
+# (where the name does NOT include the path)
+# If a tag file is not located in the directory in which doxygen 
+# is run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create 
+# a tag file that is based on the input files it reads.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES all external classes will be listed 
+# in the class index. If set to NO only the inherited external classes 
+# will be listed.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed 
+# in the modules index. If set to NO, only the current project's groups will 
+# be listed.
+
+EXTERNAL_GROUPS        = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script 
+# interpreter (i.e. the result of `which perl').
+
+PERL_PATH              = @PERL@
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool   
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will 
+# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base or
+# super classes. Setting the tag to NO turns the diagrams off. Note that this 
+# option is superceded by the HAVE_DOT option below. This is only a fallback. It is 
+# recommended to install and use dot, since it yields more powerful graphs.
+
+CLASS_DIAGRAMS         = YES
+
+# If set to YES, the inheritance and collaboration graphs will hide 
+# inheritance and usage relations if the target is undocumented 
+# or is not a class.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is 
+# available from the path. This tool is part of Graphviz, a graph visualization 
+# toolkit from AT&T and Lucent Bell Labs. The other options in this section 
+# have no effect if this option is set to NO (the default)
+
+HAVE_DOT               = YES
+
+# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen 
+# will generate a graph for each documented class showing the direct and 
+# indirect inheritance relations. Setting this tag to YES will force the 
+# the CLASS_DIAGRAMS tag to NO.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen 
+# will generate a graph for each documented class showing the direct and 
+# indirect implementation dependencies (inheritance, containment, and 
+# class references variables) of the class with other documented classes.
+
+COLLABORATION_GRAPH    = YES
+
+# If the UML_LOOK tag is set to YES doxygen will generate inheritance and 
+# collaboration diagrams in a style similiar to the OMG's Unified Modeling 
+# Language.
+
+UML_LOOK               = YES
+
+# If set to YES, the inheritance and collaboration graphs will show the
+# relations between templates and their instances.
+
+TEMPLATE_RELATIONS     = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT 
+# tags are set to YES then doxygen will generate a graph for each documented 
+# file showing the direct and indirect include dependencies of the file with 
+# other documented files.
+
+INCLUDE_GRAPH          = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and 
+# HAVE_DOT tags are set to YES then doxygen will generate a graph for each 
+# documented header file showing the documented files that directly or 
+# indirectly include this file.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH and HAVE_DOT tags are set to YES then doxygen will 
+# generate a call dependency graph for every global function or class method. 
+# Note that enabling this option will significantly increase the time of a run. 
+# So in most cases it will be better to enable call graphs for selected 
+# functions only using the \callgraph command.
+
+CALL_GRAPH             = NO
+
+# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen 
+# will graphical hierarchy of all classes instead of a textual one.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images 
+# generated by dot. Possible values are png, jpg, or gif
+# If left blank png will be used.
+
+DOT_IMAGE_FORMAT       = png
+
+# The tag DOT_PATH can be used to specify the path where the dot tool can be 
+# found. If left blank, it is assumed the dot tool can be found on the path.
+
+DOT_PATH               = 
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that 
+# contain dot files that are included in the documentation (see the 
+# \dotfile command).
+
+DOTFILE_DIRS           = 
+
+# The MAX_DOT_GRAPH_WIDTH tag can be used to set the maximum allowed width 
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than 
+# this value, doxygen will try to truncate the graph, so that it fits within 
+# the specified constraint. Beware that most browsers cannot cope with very 
+# large images.
+
+MAX_DOT_GRAPH_WIDTH    = 1024
+
+# The MAX_DOT_GRAPH_HEIGHT tag can be used to set the maximum allows height 
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than 
+# this value, doxygen will try to truncate the graph, so that it fits within 
+# the specified constraint. Beware that most browsers cannot cope with very 
+# large images.
+
+MAX_DOT_GRAPH_HEIGHT   = 1024
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the 
+# graphs generated by dot. A depth value of 3 means that only nodes reachable 
+# from the root by following a path via at most 3 edges will be shown. Nodes that 
+# lay further from the root node will be omitted. Note that setting this option to 
+# 1 or 2 may greatly reduce the computation time needed for large code bases. Also 
+# note that a graph may be further truncated if the graph's image dimensions are 
+# not sufficient to fit the graph (see MAX_DOT_GRAPH_WIDTH and MAX_DOT_GRAPH_HEIGHT). 
+# If 0 is used for the depth value (the default), the graph is not depth-constrained.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will 
+# generate a legend page explaining the meaning of the various boxes and 
+# arrows in the dot generated graphs.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will 
+# remove the intermediate dot files that are used to generate 
+# the various graphs.
+
+DOT_CLEANUP            = YES
+
+#---------------------------------------------------------------------------
+# Configuration::addtions related to the search engine   
+#---------------------------------------------------------------------------
+
+# The SEARCHENGINE tag specifies whether or not a search engine should be 
+# used. If set to NO the values of all tags below this one will be ignored.
+
+SEARCHENGINE           = NO
+
Index: /branches/eam_branch_20071023/psModules/INSTALL
===================================================================
--- /branches/eam_branch_20071023/psModules/INSTALL	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/INSTALL	(revision 15358)
@@ -0,0 +1,167 @@
+Basic Installation
+==================
+
+   These are generic installation instructions.
+
+   The `configure' shell script attempts to guess correct values for
+various system-dependent variables used during compilation.  It uses
+those values to create a `Makefile' in each directory of the package.
+It may also create one or more `.h' files containing system-dependent
+definitions.  Finally, it creates a shell script `config.status' that
+you can run in the future to recreate the current configuration, a file
+`config.cache' that saves the results of its tests to speed up
+reconfiguring, and a file `config.log' containing compiler output
+(useful mainly for debugging `configure').
+
+   If you need to do unusual things to compile the package, please try
+to figure out how `configure' could check whether to do them, and mail
+diffs or instructions to the address given in the `README' so they can
+be considered for the next release.  If at some point `config.cache'
+contains results you don't want to keep, you may remove or edit it.
+
+   The file `configure.in' is used to create `configure' by a program
+called `autoconf'.  You only need `configure.in' if you want to change
+it or regenerate `configure' using a newer version of `autoconf'.
+
+The simplest way to compile this package is:
+
+  1. `cd' to the directory containing the package's source code and type
+     `./configure' to configure the package for your system.  If you're
+     using `csh' on an old version of System V, you might need to type
+     `sh ./configure' instead to prevent `csh' from trying to execute
+     `configure' itself.
+
+     Running `configure' takes a while.  While running, it prints some
+     messages telling which features it is checking for.
+
+  2. Type `make' to compile the package.
+
+  3. Type `make install' to install the programs and any data files and
+     documentation.
+
+  4. You can remove the program binaries and object files from the
+     source code directory by typing `make clean'.  
+
+Compilers and Options
+=====================
+
+   Some systems require unusual options for compilation or linking that
+the `configure' script does not know about.  You can give `configure'
+initial values for variables by setting them in the environment.  Using
+a Bourne-compatible shell, you can do that on the command line like
+this:
+     CC=c89 CFLAGS=-O2 LIBS=-lposix ./configure
+
+Or on systems that have the `env' program, you can do it like this:
+     env CPPFLAGS=-I/usr/local/include LDFLAGS=-s ./configure
+
+Compiling For Multiple Architectures
+====================================
+
+   You can compile the package for more than one kind of computer at the
+same time, by placing the object files for each architecture in their
+own directory.  To do this, you must use a version of `make' that
+supports the `VPATH' variable, such as GNU `make'.  `cd' to the
+directory where you want the object files and executables to go and run
+the `configure' script.  `configure' automatically checks for the
+source code in the directory that `configure' is in and in `..'.
+
+   If you have to use a `make' that does not supports the `VPATH'
+variable, you have to compile the package for one architecture at a time
+in the source code directory.  After you have installed the package for
+one architecture, use `make distclean' before reconfiguring for another
+architecture.
+
+Installation Names
+==================
+
+   By default, `make install' will install the package's files in
+`/usr/local/bin', `/usr/local/man', etc.  You can specify an
+installation prefix other than `/usr/local' by giving `configure' the
+option `--prefix=PATH'.
+
+   You can specify separate installation prefixes for
+architecture-specific files and architecture-independent files.  If you
+give `configure' the option `--exec-prefix=PATH', the package will use
+PATH as the prefix for installing programs and libraries.
+Documentation and other data files will still use the regular prefix.
+
+   If the package supports it, you can cause programs to be installed
+with an extra prefix or suffix on their names by giving `configure' the
+option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'.
+
+Optional Features
+=================
+
+   Some packages pay attention to `--enable-FEATURE' options to
+`configure', where FEATURE indicates an optional part of the package.
+They may also pay attention to `--with-PACKAGE' options, where PACKAGE
+is something like `gnu-as' or `x' (for the X Window System).  The
+`README' should mention any `--enable-' and `--with-' options that the
+package recognizes.
+
+   For packages that use the X Window System, `configure' can usually
+find the X include and library files automatically, but if it doesn't,
+you can use the `configure' options `--x-includes=DIR' and
+`--x-libraries=DIR' to specify their locations.
+
+Specifying the System Type
+==========================
+
+   There may be some features `configure' can not figure out
+automatically, but needs to determine by the type of host the package
+will run on.  Usually `configure' can figure that out, but if it prints
+a message saying it can not guess the host type, give it the
+`--host=TYPE' option.  TYPE can either be a short name for the system
+type, such as `sun4', or a canonical name with three fields:
+     CPU-COMPANY-SYSTEM
+
+See the file `config.sub' for the possible values of each field.  If
+`config.sub' isn't included in this package, then this package doesn't
+need to know the host type.
+
+   If you are building compiler tools for cross-compiling, you can also
+use the `--target=TYPE' option to select the type of system they will
+produce code for and the `--build=TYPE' option to select the type of
+system on which you are compiling the package.
+
+Sharing Defaults
+================
+
+   If you want to set default values for `configure' scripts to share,
+you can create a site shell script called `config.site' that gives
+default values for variables like `CC', `cache_file', and `prefix'.
+`configure' looks for `PREFIX/share/config.site' if it exists, then
+`PREFIX/etc/config.site' if it exists.  Or, you can set the
+`CONFIG_SITE' environment variable to the location of the site script.
+A warning: not all `configure' scripts look for a site script.
+
+Operation Controls
+==================
+
+   `configure' recognizes the following options to control how it
+operates.
+
+`--cache-file=FILE'
+     Use and save the results of the tests in FILE instead of
+     `./config.cache'.  Set FILE to `/dev/null' to disable caching, for
+     debugging `configure'.
+
+`--help'
+     Print a summary of the options to `configure', and exit.
+
+`--quiet'
+`--silent'
+`-q'
+     Do not print messages saying which checks are being made.
+
+`--srcdir=DIR'
+     Look for the package's source code in directory DIR.  Usually
+     `configure' can determine that directory automatically.
+
+`--version'
+     Print the version of Autoconf used to generate the `configure'
+     script, and exit.
+
+`configure' also accepts some other, not widely useful, options.
+
Index: /branches/eam_branch_20071023/psModules/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/Makefile.am	(revision 15358)
@@ -0,0 +1,26 @@
+SUBDIRS = src test
+
+bin_SCRIPTS = psmodules-config
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA= psmodules.pc
+
+EXTRA_DIST = \
+	Doxyfile.in \
+	psmodules-config.in \
+	psmodules.pc.in \
+	autogen.sh
+
+if HAVE_DOXYGEN
+install-data-hook: doxygen
+	-$(mkdir_p) $(mandir)/man3
+	chmod 0755 $(mandir)/man3
+	-cp $(top_builddir)/docs/man/man3/* $(mandir)/man3
+
+doxygen:
+	$(DOXYGEN)
+endif
+
+CLEANFILES = $(prefix)/docs/psmodules/* *~
+
+test: check
Index: /branches/eam_branch_20071023/psModules/NEWS
===================================================================
--- /branches/eam_branch_20071023/psModules/NEWS	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/NEWS	(revision 15358)
@@ -0,0 +1,1 @@
+ 
Index: /branches/eam_branch_20071023/psModules/README
===================================================================
--- /branches/eam_branch_20071023/psModules/README	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/README	(revision 15358)
@@ -0,0 +1,1 @@
+
Index: /branches/eam_branch_20071023/psModules/TODO
===================================================================
--- /branches/eam_branch_20071023/psModules/TODO	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/TODO	(revision 15358)
@@ -0,0 +1,1 @@
+ 
Index: /branches/eam_branch_20071023/psModules/autogen.sh
===================================================================
--- /branches/eam_branch_20071023/psModules/autogen.sh	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/autogen.sh	(revision 15358)
@@ -0,0 +1,114 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+srcdir=`dirname $0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+cd $srcdir
+
+PROJECT=psmodules
+TEST_TYPE=-f
+FILE=psmodules.pc.in
+
+DIE=0
+
+if [ "`which glibtoolize 2> /dev/null`" != "" ]
+ then LIBTOOLIZE=glibtoolize
+ else LIBTOOLIZE=libtoolize
+fi
+
+ACLOCAL="aclocal $ACLOCAL_FLAGS"
+AUTOHEADER=autoheader
+AUTOMAKE=automake
+AUTOCONF=autoconf
+
+($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $LIBTOOlIZE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/libtool/"
+        DIE=1
+}
+
+($ACLOCAL --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $ACLOCAL installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOHEADER --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOHEADER installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOMAKE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOCONF --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOCONF installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+if test "$DIE" -eq 1; then
+        exit 1
+fi
+
+test $TEST_TYPE $FILE || {
+        echo "You must run this script in the top-level $PROJECT directory"
+        exit 1
+}
+
+if test -z "$*"; then
+        echo "I am going to run ./configure with no arguments - if you wish "
+        echo "to pass any to it, please specify them on the $0 command line."
+fi
+
+$LIBTOOLIZE --copy --force || echo "$LIBTOOlIZE failed"
+$ACLOCAL || echo "$ACLOCAL failed"
+$AUTOHEADER || echo "$AUTOHEADER failed"
+$AUTOMAKE --add-missing --force-missing --copy || echo "$AUTOMAKE failed"
+$AUTOCONF || echo "$AUTOCONF failed"
+
+# bypass taps bootstrap.sh
+cd ./test/tap
+$LIBTOOLIZE --copy --force || echo "$LIBTOOlIZE failed"
+$ACLOCAL || echo "$ACLOCAL failed"
+$AUTOHEADER || echo "$AUTOHEADER failed"
+$AUTOMAKE --add-missing --force-missing --copy || echo "$AUTOMAKE failed"
+$AUTOCONF || echo "$AUTOCONF failed"
+
+cd $ORIGDIR
+
+run_configure=true
+for arg in $*; do
+    case $arg in
+        --no-configure)
+            run_configure=false
+            ;;
+        *)
+            ;;
+    esac
+done
+
+if $run_configure; then
+    $srcdir/configure --enable-maintainer-mode "$@"
+    echo
+    echo "Now type 'make' to compile $PROJECT."
+else
+    echo
+    echo "Now run 'configure' and 'make' to compile $PROJECT."
+fi
Index: /branches/eam_branch_20071023/psModules/configure.ac
===================================================================
--- /branches/eam_branch_20071023/psModules/configure.ac	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/configure.ac	(revision 15358)
@@ -0,0 +1,348 @@
+AC_PREREQ(2.61)
+
+AC_INIT([psmodules],[1.1.0],[http://pan-starrs.ifa.hawaii.edu/bugzilla])
+AC_CONFIG_SRCDIR([psmodules.pc.in])
+
+dnl this enables the building of libtap
+AC_CONFIG_SUBDIRS([test/tap])
+
+AM_INIT_AUTOMAKE([1.7 foreign dist-bzip2])
+AM_CONFIG_HEADER([src/config.h])
+AM_MAINTAINER_MODE
+
+PSMODULES_LT_VERSION="1:1:0"
+AC_SUBST(PSMODULES_LT_VERSION,$PSMODULES_LT_VERSION)
+
+IPP_STDCFLAGS
+
+AC_LANG(C)
+AC_PROG_CC_C99
+AC_GNU_SOURCE
+AC_C_INLINE
+AC_C_CONST
+AC_PROG_INSTALL
+AM_PROG_LIBTOOL
+AC_SYS_LARGEFILE
+AC_FUNC_FSEEKO
+
+AC_PREFIX_DEFAULT([`pwd`])
+
+dnl build tests at the same time as the source code
+AC_ARG_ENABLE(tests,
+  [AS_HELP_STRING(--enable-tests,build tests at same time as source)],
+  [AC_MSG_RESULT(test building enabled)
+   tests=true],
+   [tests=false])
+AM_CONDITIONAL(BUILD_TESTS, test x$tests = xtrue)
+
+dnl ------------------- PERL options ---------------------
+  AC_ARG_WITH(perl,
+    [AS_HELP_STRING(--with-perl=FILE,Specify location of PERL executable.)],
+    [AC_CHECK_PROG(PERL, $withval, $withval)],
+    [AC_CHECK_PROG(PERL, perl, `which perl`)])
+    if test "$PERL" == ""
+    then
+      AC_MSG_ERROR([PERL is required.  Use --with-perl to specify its install location.])
+    fi
+    AC_SUBST(PERL,$PERL)
+
+SRCPATH='${top_srcdir}/src'
+SRCDIRS="extras config concepts camera astrom detrend imcombine objects"
+# escape two escapes at this level so \\ gets passed to the shell and \ to perl
+SRCINC=`echo "${SRCDIRS=}" | ${PERL} -pe "s|(\w+)|-I\\\\${SRCPATH=}/\1|g"`
+SRCINC="-I${SRCPATH=} ${SRCINC=}"
+SRCSUBLIBS=`echo "${SRCDIRS=}" | ${PERL} -pe "s|(\w+)|\1/libpsmodules\1.la|g"`
+AC_SUBST(SRCSUBLIBS,${SRCSUBLIBS=})
+AC_SUBST(SRCINC,${SRCINC=})
+AC_SUBST([SRCDIRS],${SRCDIRS=})
+
+dnl doxygen -------------------------------------------------------------------
+dnl doxygen doc generation is very, very slow so we're turing it off by default
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+AC_ARG_ENABLE(doxygen,
+  [AS_HELP_STRING(--enable-doxygen ,enable manpage generation)],
+  [AC_MSG_RESULT(doxygen enabled)
+    AC_PATH_PROG([DOXYGEN], [doxygen], [])
+  ],
+  [AC_MSG_RESULT([doxygen disabled])
+    doxygen=off
+  ]
+)
+AM_CONDITIONAL([HAVE_DOXYGEN], test -n "$DOXYGEN" -a "x$doxygen" != "xoff")
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------------------------------------------------
+
+AC_PATH_PROG([ERRORCODES], [psParseErrorCodes], [missing])
+if test "$ERRORCODES" = "missing" ; then
+  AC_MSG_ERROR([psParseErrorCodes is required])
+fi
+
+dnl ------------------ kapa,libkapa options -------------------------
+dnl -- libkapa implies the requirement for libpng, libjpeg as well --
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+dnl test for command-line options: use ohana-config if not supplied
+KAPA_CFLAGS_CONFIG="true"
+KAPA_LIBS_CONFIG="true"
+AC_ARG_WITH(kapa,
+[AS_HELP_STRING(--with-kapa=DIR,Specify location of libkapa)],
+[KAPA_CFLAGS="-I$withval/include" KAPA_LIBS="-L$withval/lib" 
+ KAPA_CFLAGS_CONFIG="false"       KAPA_LIBS_CONFIG="false"])
+AC_ARG_WITH(kapa-include,
+[AS_HELP_STRING(--with-kapa-include=DIR,Specify libkapa include directory.)],
+[KAPA_CFLAGS="-I$withval" KAPA_CFLAGS_CONFIG="false"])
+AC_ARG_WITH(kapa-lib,
+[AS_HELP_STRING(--with-kapa-lib=DIR,Specify libkapa library directory.)],
+[KAPA_LIBS="-L$withval" KAPA_LIBS_CONFIG="false"])
+
+echo "KAPA_CFLAGS_CONFIG: $KAPA_CFLAGS_CONFIG"
+echo "KAPA_LIBS_CONFIG: $KAPA_LIBS_CONFIG"
+echo "KAPA_CFLAGS: $KAPA_CFLAGS"
+echo "KAPA_LIBS: $KAPA_LIBS"
+
+dnl HAVE_KAPA is set to false if any of the tests fail
+HAVE_KAPA="true"
+AC_MSG_NOTICE([checking for libkapa])
+if test "$KAPA_CFLAGS_CONFIG" = "true" -o "$KAPA_LIBS_CONFIG" = "true"; then
+  AC_MSG_NOTICE([kapa info supplied by ohana-config])
+  KAPA_CONFIG=`which ohana-config`
+  AC_CHECK_FILE($KAPA_CONFIG,[],
+    [HAVE_KAPA="false"; AC_MSG_WARN([libkapa is not found: output plots disabled.  Obtain libkapa at http://kiawe.ifa.hawaii.edu/Elixir/Ohana or use --with-kapa to specify location])])
+  
+  echo "HAVE_KAPA: $HAVE_KAPA"
+  echo "KAPA_CFLAGS_CONFIG: $KAPA_CFLAGS_CONFIG"
+
+  if test "$HAVE_KAPA" = "true" -a "$KAPA_CFLAGS_CONFIG" = "true" ; then
+   AC_MSG_NOTICE([libkapa cflags info supplied by ohana-config])
+   AC_MSG_CHECKING([libkapa cflags])
+   KAPA_CFLAGS="`${KAPA_CONFIG} --cflags`"
+   AC_MSG_RESULT([${KAPA_CFLAGS}])
+  fi
+
+  if test "$HAVE_KAPA" = "true" -a "$KAPA_LIBS_CONFIG" = "true" ; then
+   AC_PATH_X
+   if test "$no_x" = "yes" ; then
+      AC_MSG_WARN([X11 not found: output plots using kapa disabled.  Use --x-includes and --x-libraries if required.])
+      HAVE_KAPA="false"
+   else
+      AC_MSG_NOTICE([libkapa ldflags info supplied by ohana-config])
+      AC_MSG_CHECKING([libkapa ldflags])
+      if test -n "$x_libraries" ; then
+            KAPA_LIBS="`${KAPA_CONFIG} --libs` -L$x_libraries -lX11"
+      else
+            KAPA_LIBS="`${KAPA_CONFIG} --libs` -lX11"
+      fi
+      if test -n "$x_includes" ; then
+            KAPA_CFLAGS="${KAPA_CFLAGS} -I$x_includes"
+      else
+            KAPA_CFLAGS="${KAPA_CFLAGS}"
+      fi
+      AC_MSG_RESULT([${KAPA_LIBS}])
+   fi
+  fi
+fi
+
+if test "$HAVE_KAPA" = "true" ; then
+ AC_MSG_NOTICE([libkapa supplied])
+ PSMODULES_CFLAGS="${PSMODULES_CFLAGS} ${KAPA_CFLAGS}"
+ PSMODULES_LIBS="${PSMODULES_LIBS} ${KAPA_LIBS}"
+else
+ AC_MSG_NOTICE([libkapa ignored])
+fi
+
+dnl HAVE_KAPA is set to false if any of the tests fail
+dnl HAVE_KAPA=true
+dnl AC_CHECK_HEADERS([kapa.h],
+dnl  [PSMODULES_CFLAGS="$PSMODULES_CFLAGS $KAPA_CFLAGS" AC_SUBST(KAPA_CFLAGS)],
+dnl  [HAVE_KAPA=false; AC_MSG_WARN([libkapa headers not found: output plots disabled.  Obtain libkapa at http://kiawe.ifa.hawaii.edu/Elixir/Ohana or use --with-kapa to specify location.])]
+dnl )
+dnl AC_CHECK_LIB(kapa,KapaInitGraph,
+dnl  [PSMODULES_LIBS="$PSMODULES_LIBS $JPEG_LDFLAGS -ljpeg"],  
+dnl  [HAVE_KAPA=false; AC_MSG_WARN([libkapa headers not found: output plots disabled.  Obtain libkapa at http://kiawe.ifa.hawaii.edu/Elixir/Ohana or use --with-kapa to specify location.])],[-lm]
+dnl )
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ libjpeg options ---------------------
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+AC_ARG_WITH(jpeg,
+[AS_HELP_STRING(--with-jpeg=DIR,Specify location of libjpeg.)],
+[JPEG_CFLAGS="-I$withval/include"
+ JPEG_LDFLAGS="-L$withval/lib"])
+AC_ARG_WITH(jpeg-include,
+[AS_HELP_STRING(--with-jpeg-include=DIR,Specify libjpeg include directory.)],
+[JPEG_CFLAGS="-I$withval"])
+AC_ARG_WITH(jpeg-lib,
+[AS_HELP_STRING(--with-jpeg-lib=DIR,Specify libjpeg library directory.)],
+[JPEG_LDFLAGS="-L$withval"])
+
+CFLAGS="${CFLAGS} ${JPEG_CFLAGS}"
+CPPFLAGS=${CFLAGS}
+LDFLAGS="${LDFLAGS} ${JPEG_LDFLAGS}"
+
+AC_CHECK_HEADERS([jpeglib.h],
+  [PSMODULES_CFLAGS="$PSMODULES_CFLAGS $JPEG_CFLAGS" AC_SUBST(JPEG_CFLAGS)],
+  [HAVE_KAPA=false; AC_MSG_WARN([libjpeg headers not found: output plots disabled.  Obtain libjpeg from http://www.ijg.org/ or use --with-jpeg to specify location.])]
+)
+
+AC_CHECK_LIB(jpeg,jpeg_CreateCompress,
+  [PSMODULES_LIBS="$PSMODULES_LIBS $JPEG_LDFLAGS -ljpeg"],
+  [HAVE_KAPA=false; AC_MSG_WARN([libjpeg library not found: output plots disabled.  Obtain libjpeg from http://www.ijg.org/ or use --with-jpeg to specify location.])]
+)
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ libpng options ---------------------
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+AC_ARG_WITH(png,
+[AS_HELP_STRING(--with-png=DIR,Specify location of libpng.)],
+[PNG_CFLAGS="-I$withval/include"
+ PNG_LDFLAGS="-L$withval/lib"])
+AC_ARG_WITH(png-include,
+[AS_HELP_STRING(--with-png-include=DIR,Specify libpng include directory.)],
+[PNG_CFLAGS="-I$withval"])
+AC_ARG_WITH(png-lib,
+[AS_HELP_STRING(--with-png-lib=DIR,Specify libpng library directory.)],
+[PNG_LDFLAGS="-L$withval"])
+
+CFLAGS="${CFLAGS} ${PNG_CFLAGS}"
+CPPFLAGS=${CFLAGS}
+LDFLAGS="${LDFLAGS} ${PNG_LDFLAGS}"
+
+AC_CHECK_HEADERS([png.h],
+  [PSMODULES_CFLAGS="$PSMODULES_CFLAGS $PNG_CFLAGS" AC_SUBST(PNG_CFLAGS)],
+  [HAVE_KAPA=false; AC_MSG_WARN([libpng headers not found: output plots disabled.  Obtain libpng from http://www.ijg.org/ or use --with-png to specify location.])]
+)
+
+AC_CHECK_LIB(png,png_init_io,
+  [PSMODULES_LIBS="$PSMODULES_LIBS $PNG_LDFLAGS -lpng"],
+  [HAVE_KAPA=false; AC_MSG_WARN([libpng library not found: output plots disabled.  Obtain libpng from http://www.ijg.org/ or use --with-png to specify location.])]
+)
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ use kapa or not? ---------------------
+
+if test "$HAVE_KAPA" == "true" ; then
+  AC_MSG_RESULT([including plotting functions])
+  AC_DEFINE([HAVE_KAPA],[1],[enable use of libkapa])
+else
+  AC_MSG_RESULT([skipping plotting functions])
+  AC_DEFINE([HAVE_KAPA],[0],[disable use of libkapa])
+fi
+
+dnl pslib ---------------------------------------------------------------------
+AC_ARG_WITH(pslib-config,
+[  --with-pslib-config=FILE  Specify location of psLib-config script],
+[PSLIB_CONFIG=$withval])
+
+if test -z ${PSLIB_CONFIG} ; then
+  PKG_CHECK_MODULES([PSLIB], [pslib >= 0.12.0])
+else
+  AC_CHECK_FILE($PSLIB_CONFIG,[],
+    [AC_MSG_ERROR([psLib is required.  If not in path, use --with-pslib-config to specify pslib-config script location.])])
+  AC_MSG_CHECKING([PSLIB_CFLAGS])
+  PSLIB_CFLAGS="`${PSLIB_CONFIG} --cflags`"
+  AC_MSG_RESULT([${PSLIB_CFLAGS}])
+  AC_MSG_CHECKING([PSLIB_LIBS])
+  PSLIB_LIBS="`${PSLIB_CONFIG} --libs`"
+  AC_MSG_RESULT([${PSLIB_LIBS}])
+fi
+
+PSMODULES_CFLAGS="${PSMODULES_CFLAGS=} ${PSLIB_CFLAGS}"
+PSMODULES_LIBS="${PSMODULES_LIBS=} ${PSLIB_LIBS}"
+
+dnl nebclient -----------------------------------------------------------------
+
+PKG_CHECK_MODULES([NEBCLIENT], [nebclient >= 0.0.2],
+    [AC_DEFINE([HAVE_NEBCLIENT], 1, [Define to 1 if libnebclient is avaiable])],    [AC_MSG_RESULT([no])]
+)
+
+PSMODULES_CFLAGS="${PSMODULES_CFLAGS=} ${NEBCLIENT_CFLAGS}"
+PSMODULES_LIBS="${PSMODULES_LIBS=} ${NEBCLIENT_LIBS}"
+
+echo "PSMODULES_CFLAGS: $PSMODULES_CFLAGS"
+echo "PSMODULE_LIBS: $PSMODULES_LIBS"
+
+dnl ------- enable -Werror after all of the probes have run ---------
+IPP_STDOPTS
+
+CFLAGS="${CFLAGS} -Wall -Werror"
+dnl enable POSIX/XSI and C99 compliance
+CFLAGS="${CFLAGS=} -D_XOPEN_SOURCE=600 -D_POSIX_C_SOURCE=200112L"
+
+dnl ---------------- export variables --------------------
+
+
+AC_SUBST([PSMODULES_CFLAGS])
+AC_SUBST([PSMODULES_LIBS])
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+  src/astrom/Makefile
+  src/camera/Makefile
+  src/config/Makefile
+  src/concepts/Makefile
+  src/detrend/Makefile
+  src/imcombine/Makefile
+  src/objects/Makefile
+  src/extras/Makefile
+  test/Makefile
+  test/astrom/Makefile
+  test/config/Makefile
+  test/camera/Makefile
+  test/concepts/Makefile
+  test/detrend/Makefile
+  test/extras/Makefile
+  test/imcombine/Makefile
+  test/objects/Makefile
+  test/pstap/Makefile
+  test/pstap/src/Makefile
+  Doxyfile
+  psmodules-config
+  psmodules.pc
+])
+
+AC_OUTPUT
Index: /branches/eam_branch_20071023/psModules/psmodules-config.in
===================================================================
--- /branches/eam_branch_20071023/psModules/psmodules-config.in	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/psmodules-config.in	(revision 15358)
@@ -0,0 +1,76 @@
+#! /bin/sh
+
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@/@PACKAGE_NAME@
+
+usage()
+{
+    cat <<EOF
+Usage: pslib-config [OPTION]
+
+Known values for OPTION are:
+
+  --prefix		print psLib installation prefix
+  --libs		print library linking information
+  --cflags		print pre-processor and compiler flags
+  --help		display this help and exit
+  --version		output version information
+
+EOF
+
+    exit $1
+}
+
+if test $# -eq 0; then
+    usage 1
+fi
+
+cflags=false
+libs=false
+
+while test $# -gt 0; do
+    case "$1" in
+    -*=*) optarg=`echo "$1" | sed 's/[-_a-zA-Z0-9]*=//'` ;;
+    *) optarg= ;;
+    esac
+
+    case "$1" in
+    --prefix=*)
+	prefix=$optarg
+	;;
+
+    --prefix)
+	echo $prefix
+	;;
+
+    --version)
+	echo @VERSION@
+	exit 0
+	;;
+
+    --help)
+	usage 0
+	;;
+
+    --cflags)
+       	echo -I${includedir} @PSMODULES_CFLAGS@
+       	;;
+
+    --libs)
+       	echo -L${libdir} -lpsmodules @PSMODULES_LIBS@
+       	;;
+
+    --deps)
+       	echo @LDFLAGS@
+       	;;
+    *)
+	usage
+	exit 1
+	;;
+    esac
+    shift
+done
+
+exit 0
Index: /branches/eam_branch_20071023/psModules/psmodules.pc.in
===================================================================
--- /branches/eam_branch_20071023/psModules/psmodules.pc.in	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/psmodules.pc.in	(revision 15358)
@@ -0,0 +1,12 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@/@PACKAGE_NAME@
+
+Name: @PACKAGE_NAME@
+Description: Pan-STARRS Module Library
+Version: @VERSION@
+Requires: pslib
+Libs: -L${libdir} -lpsmodules
+Libs.private: @PSMODULES_LIBS@
+Cflags: -I${includedir} @PSMODULES_CFLAGS@
Index: /branches/eam_branch_20071023/psModules/src/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/src/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/.cvsignore	(revision 15358)
@@ -0,0 +1,9 @@
+.deps
+.libs
+Makefile
+config.h
+stamp-h1
+Makefile.in
+config.h.in
+*.la
+*.lo
Index: /branches/eam_branch_20071023/psModules/src/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/src/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+SUBDIRS = $(SRCDIRS)
+lib_LTLIBRARIES = libpsmodules.la
+
+libpsmodules_la_CPPFLAGS = $(SRCINC)
+libpsmodules_la_LIBADD = $(SRCSUBLIBS) $(PSMODULES_LIBS)
+libpsmodules_la_DEPENDENCIES = $(SRCSUBLIBS)
+libpsmodules_la_SOURCES = 
+libpsmodules_la_LDFLAGS = -version-info $(PSMODULES_LT_VERSION)
+
+pkginclude_HEADERS = \
+	psmodules.h
+
+CLEANFILES = *~
Index: /branches/eam_branch_20071023/psModules/src/astrom/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/.cvsignore	(revision 15358)
@@ -0,0 +1,6 @@
+.deps
+.libs
+Makefile
+Makefile.in
+*.la
+*.lo
Index: /branches/eam_branch_20071023/psModules/src/astrom/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/Makefile.am	(revision 15358)
@@ -0,0 +1,19 @@
+noinst_LTLIBRARIES = libpsmodulesastrom.la
+
+libpsmodulesastrom_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS) -I../pslib/
+libpsmodulesastrom_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulesastrom_la_SOURCES  = \
+	pmAstrometryObjects.c \
+	pmAstrometryRegions.c \
+	pmAstrometryDistortion.c \
+	pmAstrometryUtils.c \
+	pmAstrometryWCS.c
+
+pkginclude_HEADERS = \
+	pmAstrometryObjects.h \
+	pmAstrometryRegions.h \
+	pmAstrometryDistortion.h \
+	pmAstrometryUtils.h \
+	pmAstrometryWCS.h
+
+CLEANFILES = *~
Index: /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryDistortion.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryDistortion.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryDistortion.c	(revision 15358)
@@ -0,0 +1,346 @@
+/** @file  pmAstrometryDistortion.c
+*
+*  @brief This file defines the basic types for measuring the focal-plane distortion.
+*
+*  @ingroup AstroImage
+*
+*  @author EAM, IfA
+*
+*  @version $Revision: 1.20 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2007-10-09 19:27:04 $
+*
+*  Copyright 2006 Institute for Astronomy, University of Hawaii
+*/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+/******************************************************************************/
+/*  INCLUDE FILES                                                             */
+/******************************************************************************/
+
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAExtent.h"
+#include "pmAstrometryObjects.h"
+#include "pmAstrometryRegions.h"
+#include "pmAstrometryDistortion.h"
+#include "pmKapaPlots.h"
+
+static void pmAstromGradientFree (pmAstromGradient *grad)
+{
+
+    if (grad == NULL)
+        return;
+
+    return;
+}
+
+pmAstromGradient *pmAstromGradientAlloc ()
+{
+
+    pmAstromGradient *gradient = psAlloc (sizeof(pmAstromGradient));
+    psMemSetDeallocator(gradient, (psFreeFunc) pmAstromGradientFree);
+
+    return (gradient);
+}
+
+psArray *pmAstromMeasureGradients(psArray *gradients, psArray *rawstars, psArray *refstars, psArray *matches, psRegion *region, int Nx, int Ny)
+{
+
+    if (gradients == NULL) {
+        gradients = psArrayAllocEmpty (100);
+    }
+
+    // NOTE: region specifies the FP region in pixels covered by the chip (NOT in FP units)
+    // determine range
+    int DX = (region->x1 - region->x0) / Nx;
+    int DY = (region->y1 - region->y0) / Ny;
+
+    psPolynomial2D *local = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, 1, 1);
+    local->coeffMask[1][1] = PS_POLY_MASK_SET;
+
+    // measure gradient for fractional chip regions
+    for (int nx = 0; nx < Nx; nx++) {
+        for (int ny = 0; ny < Ny; ny++) {
+            int Xmin = nx*DX;
+            int Xmax = Xmin + DX;
+            int Ymin = ny*DY;
+            int Ymax = Ymin + DY;
+
+            psStats *stats = NULL;
+            psVector *mask = NULL;
+            pmAstromGradient *grad = NULL;
+
+            psVector *L  = psVectorAllocEmpty (100, PS_TYPE_F32);
+            psVector *M  = psVectorAllocEmpty (100, PS_TYPE_F32);
+            psVector *dP = psVectorAllocEmpty (100, PS_TYPE_F32);
+            psVector *dQ = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+            // XXX this is a bit inefficient: first sorting by X or Y could speed this up.
+            // XXX or assigning to a segment in a single pass first
+            // select the stars within this chip region
+            int Npts = 0;
+            for (int i = 0; i < matches->n; i++) {
+
+                pmAstromMatch *match = matches->data[i];
+
+                pmAstromObj *raw = rawstars->data[match->raw];
+
+                if (raw->chip->x < Xmin) continue;
+                if (raw->chip->x > Xmax) continue;
+                if (raw->chip->y < Ymin) continue;
+                if (raw->chip->y > Ymax) continue;
+
+                pmAstromObj *ref = refstars->data[match->ref];
+
+                L->data.F32[Npts] = ref->FP->x;
+                M->data.F32[Npts] = ref->FP->y;
+
+                // P,Q = L,M + terms of order epsilon.
+                // measuring the gradient constrains thos terms
+                dP->data.F32[Npts] = ref->TP->x - raw->FP->x;
+                dQ->data.F32[Npts] = ref->TP->y - raw->FP->y;
+
+                psVectorExtend (L, 100, 1);
+                psVectorExtend (M, 100, 1);
+                psVectorExtend (dP, 100, 1);
+                psVectorExtend (dQ, 100, 1);
+                Npts++;
+            }
+	    
+            psTrace ("psModules.astrom", 4, "Npts: %d (%d,%d) : (%d - %d),(%d - %d)\n", Npts, nx, ny, Xmin, Xmax, Ymin, Ymax);
+
+            if (Npts < 5)
+                goto skip;
+
+            // stats structure and mask for use in measuring the clipping statistic
+            // this analysis has too few data points to use the robust median method
+            stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+            mask = psVectorAlloc (Npts, PS_TYPE_MASK);
+            psVectorInit (mask, 0);
+
+            grad = pmAstromGradientAlloc ();
+
+            // fit the collection of positions and offsets with a local 1st order gradient
+            // apply 3hi/3lo sigma clipping to the fitted data values
+            // the mask is used to mark the points which pass / fail the fit
+            if (!psVectorClipFitPolynomial2D (local, stats, mask, 0xff, dP, NULL, L, M)) {
+                goto skip;
+            }
+
+            grad->dTPdL.x = local->coeff[1][0];
+            grad->dTPdM.x = local->coeff[0][1];
+
+            // fit the collection of positions and offsets with a local 1st order gradient
+            // apply 3hi/3lo sigma clipping to the fitted data values
+            // the mask is used to mark the points which pass / fail the fit
+            if (!psVectorClipFitPolynomial2D (local, stats, mask, 0xff, dQ, NULL, L, M)) {
+                goto skip;
+            }
+
+            grad->dTPdL.y = local->coeff[1][0];
+            grad->dTPdM.y = local->coeff[0][1];
+
+            // also measure the L and M median positions as a representative coordinate
+            psVectorStats (stats, L, NULL, NULL, 0);
+            grad->FP.x = stats->sampleMedian;
+
+            psVectorStats (stats, M, NULL, NULL, 0);
+            grad->FP.y = stats->sampleMedian;
+
+            psArrayAdd (gradients, 100, grad);
+
+skip:
+            psFree (grad);
+            psFree (stats);
+            psFree (mask);
+            psFree (L);
+            psFree (M);
+            psFree (dP);
+            psFree (dQ);
+        }
+    }
+    psFree (local);
+    return gradients;
+}
+
+bool pmAstromFitDistortion(pmFPA *fpa, psArray *gradients, double pixelScale)
+{
+
+    psPolynomial2D *localX = NULL;
+    psPolynomial2D *localY = NULL;
+
+    // assign the gradient elements to psVectors for fitting
+    psVector *dPdL = psVectorAlloc (gradients->n, PS_TYPE_F32);
+    psVector *dQdL = psVectorAlloc (gradients->n, PS_TYPE_F32);
+    psVector *dPdM = psVectorAlloc (gradients->n, PS_TYPE_F32);
+    psVector *dQdM = psVectorAlloc (gradients->n, PS_TYPE_F32);
+    psVector *L = psVectorAlloc (gradients->n, PS_TYPE_F32);
+    psVector *M = psVectorAlloc (gradients->n, PS_TYPE_F32);
+
+    for (int i = 0; i < gradients->n; i++) {
+
+        pmAstromGradient *grad = gradients->data[i];
+
+        dPdL->data.F32[i] = grad->dTPdL.x;
+        dQdL->data.F32[i] = grad->dTPdL.y;
+
+        dPdM->data.F32[i] = grad->dTPdM.x;
+        dQdM->data.F32[i] = grad->dTPdM.y;
+
+        L->data.F32[i] = grad->FP.x;
+        M->data.F32[i] = grad->FP.y;
+    }
+
+    // mask and stats structure for measuring the clipping statistic
+    // this analysis has too few data points to use the robust median method
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    psVector *mask = psVectorAlloc (gradients->n, PS_TYPE_MASK);
+    psVectorInit (mask, 0);
+
+    // the order of the gradient fits need to be 1 less than the distortion term
+    // determine the gradient order(s) from the fpa->toTP structure
+    localX = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, fpa->toTPA->x->nX-1, fpa->toTPA->x->nY);
+    localY = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, fpa->toTPA->x->nX,   fpa->toTPA->x->nY-1);
+
+    // set masks based on fpa->toTPA
+    for (int i = 0; i <= fpa->toTPA->x->nX; i++) {
+        for (int j = 0; j <= fpa->toTPA->x->nY; j++) {
+	    if ((i > 0) && (i <= fpa->toTPA->x->nX)) {
+		localX->coeffMask[i-1][j] = fpa->toTPA->x->coeffMask[i][j];
+	    }
+	    if ((j > 0) && (j <= fpa->toTPA->x->nY)) {
+		localY->coeffMask[i][j-1] = fpa->toTPA->x->coeffMask[i][j];
+	    }
+        }
+    }
+
+    // fit the local gradients in each direction
+    if (!psVectorClipFitPolynomial2D (localX, stats, mask, 0xff, dPdL, NULL, L, M)) {
+        psLogMsg ("psastro", 3, "failed to fit x-dir gradient\n");
+	psFree (localX);
+	psFree (localY);
+        goto escape;
+    }
+
+    if (!psVectorClipFitPolynomial2D (localY, stats, mask, 0xff, dPdM, NULL, L, M)) {
+        psLogMsg ("psastro", 3, "failed to fit y-dir gradient\n");
+	psFree (localX);
+	psFree (localY);
+        goto escape;
+    }
+
+    // update fpa->toTP distortion terms
+    fpa->toTPA->x->coeff[0][0] = 0;
+    for (int i = 1; i <= fpa->toTPA->x->nX; i++) {
+        if (fpa->toTPA->x->coeffMask[i][0] & PS_POLY_MASK_SET) {
+	    continue;
+	}
+	fpa->toTPA->x->coeff[i][0] = localX->coeff[i-1][0] / i;
+    }
+    for (int j = 1; j <= fpa->toTPA->x->nY; j++) {
+        if (fpa->toTPA->x->coeffMask[0][j] & PS_POLY_MASK_SET) {
+	    continue;
+	}
+	fpa->toTPA->x->coeff[0][j] = localY->coeff[0][j-1] / j;
+    }
+    for (int i = 1; i <= fpa->toTPA->x->nX; i++) {
+        for (int j = 1; j <= fpa->toTPA->x->nY; j++) {
+            if (fpa->toTPA->x->coeffMask[i][j] & PS_POLY_MASK_SET) {
+		continue;
+	    }
+	    fpa->toTPA->x->coeff[i][j] = 0.5*(localX->coeff[i-1][j] / i + localY->coeff[i][j-1] / j);
+        }
+    }
+    fpa->toTPA->x->coeff[1][0] += 1.0;
+    psFree (localX);
+    psFree (localY);
+
+    // the order of the gradient fits need to be 1 less than the distortion term
+    // determine the gradient order(s) from the fpa->toTP structure
+    localX = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, fpa->toTPA->y->nX-1, fpa->toTPA->y->nY);
+    localY = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, fpa->toTPA->y->nX,   fpa->toTPA->y->nY-1);
+
+    // set masks based on fpa->toTP
+    for (int i = 0; i < fpa->toTPA->y->nX; i++) {
+        for (int j = 0; j < fpa->toTPA->y->nY; j++) {
+	    if ((i > 0) && (i <= fpa->toTPA->y->nX)) {
+		localX->coeffMask[i-1][j] = fpa->toTPA->y->coeffMask[i][j];
+	    }
+	    if ((j > 0) && (j <= fpa->toTPA->y->nY)) {
+		localY->coeffMask[i][j-1] = fpa->toTPA->y->coeffMask[i][j];
+	    }
+        }
+    }
+
+    // fit the local gradients in each direction
+    psVectorClipFitPolynomial2D (localX, stats, mask, 0xff, dQdL, NULL, L, M);
+    psVectorClipFitPolynomial2D (localY, stats, mask, 0xff, dQdM, NULL, L, M);
+
+    // update fpa->toTP distortion terms
+    fpa->toTPA->y->coeff[0][0] = 0;
+    for (int i = 1; i <= fpa->toTPA->y->nX; i++) {
+        if (fpa->toTPA->y->coeffMask[i][0] & PS_POLY_MASK_SET) {
+	    continue;
+        }
+	fpa->toTPA->y->coeff[i][0] = localX->coeff[i-1][0] / i;
+    }
+    for (int j = 1; j <= fpa->toTPA->y->nY; j++) {
+        if (fpa->toTPA->y->coeffMask[0][j] & PS_POLY_MASK_SET) {
+	    continue;
+        }
+	fpa->toTPA->y->coeff[0][j] = localY->coeff[0][j-1] / j;
+    }
+    for (int i = 1; i <= fpa->toTPA->y->nX; i++) {
+        for (int j = 1; j <= fpa->toTPA->y->nY; j++) {
+            if (fpa->toTPA->y->coeffMask[i][j] & PS_POLY_MASK_SET) {
+		continue;
+            }
+	    fpa->toTPA->y->coeff[i][j] = 0.5*(localX->coeff[i-1][j] / i + localY->coeff[i][j-1] / j);
+        }
+    }
+    fpa->toTPA->y->coeff[0][1] += 1.0;
+    psFree (localX);
+    psFree (localY);
+
+    // free unneeded structures
+    psFree (dPdL);
+    psFree (dPdM);
+    psFree (dQdL);
+    psFree (dQdM);
+    psFree (L);
+    psFree (M);
+    psFree (stats);
+    psFree (mask);
+
+    // reset the fromTPA terms here. choose an appropriate region based on the dimensions of
+    // the complete FPA
+    psRegion *region = pmAstromFPAExtent (fpa);
+
+    psFree (fpa->fromTPA);
+    fpa->fromTPA = psPlaneTransformInvert(NULL, fpa->toTPA, *region, 50);
+    psFree (region);
+
+    if (fpa->fromTPA == NULL) {
+        psError (PS_ERR_UNKNOWN, false, "failed to invert fpa->toTPA\n");
+	return false;
+    }
+
+    return true;
+
+escape:
+    // free unneeded structures
+    psFree (dPdL);
+    psFree (dPdM);
+    psFree (dQdL);
+    psFree (dQdM);
+    psFree (L);
+    psFree (M);
+    psFree (stats);
+    psFree (mask);
+    return false;
+}
Index: /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryDistortion.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryDistortion.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryDistortion.h	(revision 15358)
@@ -0,0 +1,59 @@
+/* @file  pmAstrometryDistortion.h
+ * @brief This file defines the basic types for measuring and fitting the focal-plane distortion.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-18 22:07:17 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_DISTORTION_H
+#define PM_ASTROMETRY_DISTORTION_H
+
+/// @addtogroup Astrometry
+/// @{
+
+/* The following data structure carries the information about the residual
+ * gradient of source positions in the tangent plane (pmAstromObj.TP) as a
+ * function of position in the focal plane (pmAstromObj.FP).
+ */
+typedef struct
+{
+    psPlane FP;
+    psPlane dTPdL;
+    psPlane dTPdM;
+}
+pmAstromGradient;
+
+pmAstromGradient *pmAstromGradientAlloc ();
+
+/* The following function determines the position residual, in the tangent
+ * plane, as a function of position in the focal plane, for a collection of raw
+ * measurements and matched reference stars. The configuration data must include
+ * the bin size over which the gradient is measured (keyword: ASTROM.GRAD.BOX).
+ * The function returns an array of pmAstromGradient structures, defined below.
+ */
+psArray *pmAstromMeasureGradients(
+    psArray *gradients,
+    psArray *rawstars,
+    psArray *refstars,
+    psArray *matches,
+    psRegion *region,
+    int Nx, int Ny
+);
+
+/* The gradient set measured above can be fitted with a pair of 2D
+ * polynomials. The resulting fits can then be related back to the implied
+ * polynomials which represent the distortion. The following function performs
+ * the fit and applies the result to the distortion transformation of the
+ * supplied pmFPA structure. The configuration variable supplies the polynomial
+ * order (keyword: ASTROM.DISTORT.ORDER).
+ */
+bool pmAstromFitDistortion(
+    pmFPA *fpa,
+    psArray *gradients,
+    double pixelScale);
+
+/// @}
+#endif // PM_ASTROMETRY_DISTORTION_H
Index: /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryObjects.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryObjects.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryObjects.c	(revision 15358)
@@ -0,0 +1,887 @@
+/** @file  pmAstrometryObjects.c
+*
+*  @brief This file defines the basic types for matching objects
+*  based on their astrometry.
+*
+*  @ingroup AstroImage
+*
+*  @author EAM, IfA
+*
+*  @version $Revision: 1.34 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2007-09-29 02:28:04 $
+*
+*  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+*/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+/******************************************************************************/
+/*  INCLUDE FILES                                                             */
+/******************************************************************************/
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <unistd.h>   // for unlink
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmAstrometryObjects.h"
+
+# define DEG_RAD 57.295779513082322
+# define RAD_DEG  0.017453292519943
+
+#define PM_ASTROMETRYOBJECTS_DEBUG 1
+/******************************************************************************
+pmAstromObjSortByMag(**a, **b): sort by mag (descending)
+
+Is this a private routine?
+Should we do the early asserts?
+ ******************************************************************************/
+int pmAstromObjSortByMag(
+    const void **a,
+    const void **b)
+{
+    if (PM_ASTROMETRYOBJECTS_DEBUG) {
+        PS_ASSERT_PTR_NON_NULL(a, 0);
+        PS_ASSERT_PTR_NON_NULL(*a, 0);
+        PS_ASSERT_PTR_NON_NULL(b, 0);
+        PS_ASSERT_PTR_NON_NULL(*b, 0);
+    }
+
+    pmAstromObj *A = *(pmAstromObj **)a;
+    pmAstromObj *B = *(pmAstromObj **)b;
+
+    psF32 diff = A->Mag - B->Mag;
+    if (diff > FLT_EPSILON) {
+        return (-1);
+    }
+
+    if (diff < FLT_EPSILON) {
+        return (+1);
+    }
+
+    return (0);
+}
+
+/************************************************************************************************************/
+/*
+ * Working routine to match two lists (where x[12] are sorted), given psVectors of their coordinates and the
+ * permutation used to sort in x
+ */
+static psArray *match_lists(const psVector *x1, const psVector *y1, // x/y coordinates of first set of objects
+                            const psVector *x2, const psVector *y2, // x/y   "    "    "  second "   "  "   "
+                            const psVector *sorted1, const psVector *sorted2, // mapping to original order
+                            const double RADIUS) // matching radius
+{
+    psArray *matches = psArrayAllocEmpty(x1->n);
+
+    const double RADIUS_SQR = PS_SQR(RADIUS);
+    double dX, dY, dR;
+
+    int jStart;
+    int i = 0, j = 0;
+    while (i < x1->n && j < x2->n) {
+        dX = x1->data.F64[i] - x2->data.F64[j];
+        if (dX <= -RADIUS) {
+            i++;
+            continue;
+        }
+        if (dX >= +RADIUS) {
+            j++;
+            continue;
+        }
+
+        jStart = j;
+        while (fabs(dX) < RADIUS && j < x2->n) {
+
+            dX = x1->data.F64[i] - x2->data.F64[j];
+            dY = y1->data.F64[i] - y2->data.F64[j];
+            dR = dX*dX + dY*dY;
+
+            if (dR > RADIUS_SQR) {
+                j++;
+                continue;
+            }
+
+            // got a match; add to output list
+            pmAstromMatch *match = pmAstromMatchAlloc (sorted1->data.S32[i], sorted2->data.S32[j]);
+            psArrayAdd (matches, 100, match);
+            psFree (match);
+
+            j++;
+        }
+        j = jStart;
+        i++;
+    }
+
+    return (matches);
+}
+
+/************************************************************************************************************/
+// macro to generate code for radius match function based on desired member
+// radius is in units of matching member (eg, pixels for chip, microns for FP, etc)
+#define MAKE_ASTROM_RADIUS(FUNC, MEMBER) \
+psArray *FUNC( \
+               const psArray *st1, \
+               const psArray *st2, \
+               double RADIUS) \
+{ \
+    PS_ASSERT_PTR_NON_NULL(st1, NULL); \
+    PS_ASSERT_PTR_NON_NULL(st2, NULL); \
+    \
+    assert(st1->n == 0 || pmAstromObjTest(st1->data[0])); \
+    assert(st2->n == 0 || pmAstromObjTest(st2->data[0])); \
+    \
+    /* sort both lists by X coord; st1 first */ \
+    psVector *x1 = psVectorAlloc(st1->n, PS_TYPE_F64); \
+    for (int i = 0; i < st1->n; i++) { \
+        x1->data.F64[i] = ((pmAstromObj *)st1->data[i])->MEMBER->x; \
+    } \
+    const psVector *sorted1 = psVectorSortIndex(NULL, x1); \
+    assert (sorted1->type.type == PS_TYPE_S32); \
+    \
+    psVector *y1 = psVectorAlloc(st1->n, PS_TYPE_F64); \
+    for (int i = 0; i < st1->n; i++) { \
+        x1->data.F64[i] = ((pmAstromObj *)st1->data[sorted1->data.S32[i]])->MEMBER->x; \
+        y1->data.F64[i] = ((pmAstromObj *)st1->data[sorted1->data.S32[i]])->MEMBER->y; \
+    } \
+    \
+    /* now st2 */ \
+    psVector *x2 = psVectorAlloc(st2->n, PS_TYPE_F64); \
+    for (int i = 0; i < st2->n; i++) { \
+        x2->data.F64[i] = ((pmAstromObj *)st2->data[i])->MEMBER->x; \
+    } \
+    const psVector *sorted2 = psVectorSortIndex(NULL, x2); \
+    \
+    psVector *y2 = psVectorAlloc(st2->n, PS_TYPE_F64); \
+    for (int i = 0; i < st2->n; i++) { \
+        x2->data.F64[i] = ((pmAstromObj *)st2->data[sorted2->data.S32[i]])->MEMBER->x; \
+        y2->data.F64[i] = ((pmAstromObj *)st2->data[sorted2->data.S32[i]])->MEMBER->y; \
+    } \
+    /* Do the work */ \
+    psArray *matches = match_lists(x1, y1, x2, y2, sorted1, sorted2, RADIUS); \
+    \
+    psFree((void *)sorted1); \
+    psFree((void *)sorted2); \
+    psFree(x1); \
+    psFree(y1); \
+    psFree(x2); \
+    psFree(y2); \
+    \
+    psLogMsg (__func__, 3, "radius match: %ld pairs (radius: %f)\n", matches->n, RADIUS); \
+    return (matches); \
+}
+
+/******************************************************************************/
+/*
+ * Match two lists of pmAstromObjs, based on the FP, TP, or chip coordinates
+ */
+MAKE_ASTROM_RADIUS(pmAstromRadiusMatch, FP)
+MAKE_ASTROM_RADIUS(pmAstromRadiusMatchFP, FP)
+MAKE_ASTROM_RADIUS(pmAstromRadiusMatchTP, TP)
+MAKE_ASTROM_RADIUS(pmAstromRadiusMatchChip, chip)
+
+/******************************************************************************
+pmAstromMatchFit(map, raw, ref, match, stats): take two matched star lists
+and fit a psPlaneTransform between them
+ ******************************************************************************/
+pmAstromFitResults *pmAstromMatchFit(
+    psPlaneTransform *map,
+    psArray *raw,
+    psArray *ref,
+    psArray *match,
+    psStats *stats)
+{
+    PS_ASSERT_PTR_NON_NULL(map, NULL);
+    PS_ASSERT_PTR_NON_NULL(raw, NULL);
+    PS_ASSERT_PTR_NON_NULL(ref, NULL);
+    PS_ASSERT_PTR_NON_NULL(match, NULL);
+    PS_ASSERT_PTR_NON_NULL(stats, NULL);
+
+    // reassign values for clip fit
+    // XXX set wt based on mag error?
+    psVector *X = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *Y = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *x = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *y = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *wt = psVectorAlloc (match->n, PS_TYPE_F32);
+    // take the matched stars, first fit
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *rawStar = raw->data[pair->raw];
+        pmAstromObj *refStar = ref->data[pair->ref];
+
+        X->data.F32[i] = rawStar->chip->x;
+        Y->data.F32[i] = rawStar->chip->y;
+
+        x->data.F32[i] = refStar->FP->x;
+        y->data.F32[i] = refStar->FP->y;
+
+        wt->data.F32[i] = 1.0;
+    }
+
+    // constant errors
+    psVector *mask = psVectorAlloc (match->n, PS_TYPE_U8);
+    psVectorInit (mask, 0);
+
+    // the stats options supplied are used to perform the clip fitting
+    pmAstromFitResults *results = pmAstromFitResultsAlloc();
+    results->xStats = psStatsAlloc (PS_STAT_NONE);
+    results->yStats = psStatsAlloc (PS_STAT_NONE);
+    *results->xStats = *stats;
+    *results->yStats = *stats;
+
+    int nIter = stats->clipIter;
+
+    results->xStats->clipIter = 1;
+    results->yStats->clipIter = 1;
+
+    // fit chip-to-FPA transformation
+    // we run 'clipIter' cycles clipping in each of x and y, with only one iteration each
+    // need to use the stats lookups functions to get the width and center
+    for (int i = 0; i < nIter; i++) {
+        psVectorClipFitPolynomial2D (map->x, results->xStats, mask, 0xff, x, wt, X, Y);
+        // psTrace ("psModules.astrom", 3, "x resid: %f +/- %f (%ld of %ld)\n", results->xStats->robustMedian, results->xStats->robustStdev, results->xStats->clippedNvalues, x->n);
+        psTrace ("psModules.astrom", 3, "x resid: %f +/- %f (%ld of %ld)\n", results->xStats->clippedMean, results->xStats->clippedStdev, results->xStats->clippedNvalues, x->n);
+
+        psVectorClipFitPolynomial2D (map->y, results->yStats, mask, 0xff, y, wt, X, Y);
+        // psTrace ("psModules.astrom", 3, "y resid: %f +/- %f (%ld of %ld)\n", results->yStats->robustMedian, results->yStats->robustStdev, results->yStats->clippedNvalues, y->n);
+        psTrace ("psModules.astrom", 3, "y resid: %f +/- %f (%ld of %ld)\n", results->yStats->clippedMean, results->yStats->clippedStdev, results->yStats->clippedNvalues, y->n);
+    }
+    results->xStats->clipIter = stats->clipIter;
+    results->yStats->clipIter = stats->clipIter;
+
+    psFree (x);
+    psFree (y);
+    psFree (X);
+    psFree (Y);
+    psFree (wt);
+    psFree (mask);
+
+    return (results);
+}
+
+
+/******************************************************************************
+pmAstromRotateObj(old, center, angle): rotate the focal-plane coordinates
+about the center coordinate angle specified in radians
+ ******************************************************************************/
+psArray *pmAstromRotateObj(
+    const psArray *old,
+    psPlane center,
+    double angle)
+{
+    PS_ASSERT_PTR_NON_NULL(old, NULL);
+
+    double X, Y;
+    pmAstromObj *newObj;
+    const pmAstromObj *oldObj;
+
+    psArray *new = psArrayAlloc (old->n);
+    double cs = cos(angle);
+    double sn = sin(angle);
+    double xCenter = center.x;
+    double yCenter = center.y;
+
+    for (int i = 0; i < old->n; i++) {
+
+        oldObj = (pmAstromObj *)old->data[i];
+        newObj = pmAstromObjCopy (oldObj);
+
+        X = oldObj->FP->x - xCenter;
+        Y = oldObj->FP->y - yCenter;
+
+        newObj->FP->x = X*cs + Y*sn + xCenter;
+        newObj->FP->y = Y*cs - X*sn + yCenter;
+
+        new->data[i] = newObj;
+    }
+    return (new);
+}
+
+/******************************************************************************
+pmAstromStatsFree(stats)
+ ******************************************************************************/
+static void pmAstromStatsFree(pmAstromStats *stats)
+{
+    if (stats == NULL)
+        return;
+    return;
+}
+
+/******************************************************************************
+pmAstromStatsAlloc()
+ ******************************************************************************/
+pmAstromStats *pmAstromStatsAlloc(void)
+{
+    pmAstromStats *stats = psAlloc (sizeof(pmAstromStats));
+    psMemSetDeallocator (stats, (psFreeFunc)pmAstromStatsFree);
+
+    //    stats->center = {0, 0, 0, 0};
+    //    stats->offset = {0, 0, 0, 0};
+    stats->angle     = 0.0;
+    stats->minMetric = 0.0;
+    stats->minVar    = 0.0;
+    stats->nMatch    = 0;
+    stats->nTest     = 0;
+    stats->nSigma    = 0;
+
+    return (stats);
+}
+
+/******************************************************************************
+pmAstromFitResultsFree(stats)
+ ******************************************************************************/
+static void pmAstromFitResultsFree(pmAstromFitResults *results)
+{
+    if (results == NULL)
+        return;
+    psFree (results->xStats);
+    psFree (results->yStats);
+    return;
+}
+
+/******************************************************************************
+pmAstromFitResultsAlloc()
+ ******************************************************************************/
+pmAstromFitResults *pmAstromFitResultsAlloc(void)
+{
+    pmAstromFitResults *results = psAlloc (sizeof(pmAstromFitResults));
+    psMemSetDeallocator (results, (psFreeFunc)pmAstromFitResultsFree);
+
+    results->xStats    = NULL;
+    results->yStats    = NULL;
+    results->nMatch    = 0;
+    results->nSigma    = 0;
+
+    return (results);
+}
+
+/******************************************************************************
+astromObjFree(obj)
+ ******************************************************************************/
+static void astromObjFree(pmAstromObj *obj)
+{
+    if (obj == NULL) {
+        return;
+    }
+
+    psFree(obj->pix);
+    psFree(obj->cell);
+    psFree(obj->chip);
+    psFree(obj->FP);
+    psFree(obj->TP);
+    psFree(obj->sky);
+
+    return;
+}
+
+
+/******************************************************************************
+pmAstromObjAlloc()
+ ******************************************************************************/
+pmAstromObj *pmAstromObjAlloc(void)
+{
+    pmAstromObj *obj = psAlloc (sizeof(pmAstromObj));
+    psMemSetDeallocator (obj, (psFreeFunc)astromObjFree);
+
+    obj->pix  = psPlaneAlloc();
+    obj->cell = psPlaneAlloc();
+    obj->chip = psPlaneAlloc();
+    obj->FP   = psPlaneAlloc();
+    obj->TP   = psPlaneAlloc();
+    obj->sky  = psSphereAlloc();
+    obj->Mag  = 0;
+    obj->dMag = 0;
+
+    return (obj);
+}
+
+bool pmAstromObjTest(const psPtr ptr)
+{
+    return (psMemGetDeallocator(ptr) == (psFreeFunc)astromObjFree);
+}
+
+
+
+/******************************************************************************
+pmAstromObjCopy(old)
+ ******************************************************************************/
+pmAstromObj *pmAstromObjCopy(const pmAstromObj *old)
+{
+    PS_ASSERT_PTR_NON_NULL(old, NULL);
+    pmAstromObj *obj = pmAstromObjAlloc();
+
+    *obj->pix  = *old->pix;
+    *obj->cell = *old->cell;
+    *obj->chip = *old->chip;
+    *obj->FP   = *old->FP;
+    *obj->TP   = *old->TP;
+    *obj->sky  = *old->sky;
+    obj->Mag   =  old->Mag;
+    obj->dMag  =  old->dMag;
+
+    return(obj);
+}
+
+
+/******************************************************************************
+ ******************************************************************************/
+static void pmAstromMatchFree (pmAstromMatch *match)
+{
+    if (match == NULL)
+        return;
+    return;
+}
+
+
+/******************************************************************************
+ ******************************************************************************/
+pmAstromMatch *pmAstromMatchAlloc(
+    int raw,
+    int ref)
+{
+    pmAstromMatch *match = psAlloc (sizeof(pmAstromMatch));
+    psMemSetDeallocator(match, (psFreeFunc) pmAstromMatchFree);
+
+    match->raw = raw;
+    match->ref = ref;
+
+    return (match);
+}
+
+
+static double maxOffpix;                // maximum allowed offset between lists, in raw pixels
+static double Scale;                    // grid pixel scale static
+double Offset;                          // deltas to pixels
+/******************************************************************************
+AstromGridBin(*dx, *dy, dX, dY): local function to convert x,y coords to grid
+bins it requires the globals defined above.
+
+ ******************************************************************************/
+static bool AstromGridBin(
+    int *dx,
+    int *dy,
+    double dX,
+    double dY)
+{
+    if (PM_ASTROMETRYOBJECTS_DEBUG) {
+        PS_ASSERT_PTR_NON_NULL(dx, false);
+        PS_ASSERT_PTR_NON_NULL(dy, false);
+    }
+
+    if (!isfinite(dX)) return false;
+    if (!isfinite(dY)) return false;
+
+    if (fabs(dX) > maxOffpix) return false;
+    if (fabs(dY) > maxOffpix) return false;
+
+    *dx = dX / Scale + Offset;
+    *dy = dY / Scale + Offset;
+    return true;
+}
+
+
+/******************************************************************************
+pmAstromGridAngle(raw, ref, config): match the two lists using the binned
+delta-delta max.
+ ******************************************************************************/
+pmAstromStats *pmAstromGridAngle(
+    const psArray *raw,
+    const psArray *ref,
+    const psMetadata *config)
+{
+    PS_ASSERT_PTR_NON_NULL(raw, NULL);
+    PS_ASSERT_PTR_NON_NULL(ref, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    bool status;
+    int nPix;       // size of matching grid
+    int nPixHalf;   // half-size of matching grid
+    double dX, dY;  // offset between a possible matched pair
+    int iX, iY;     // corresponding grid bin
+
+    const pmAstromObj *ob1, *ob2; // short-cut pointers to the objects
+
+    pmAstromStats *stats = pmAstromStatsAlloc();    // output match statistics
+
+    // max allowed offset in either X or Y directions
+    double gridOffset = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.OFFSET");
+
+    // sampling scale of the grid
+    double gridScale  = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.SCALE");
+
+    // set the static scaling factors
+    nPixHalf = (int)(gridOffset / gridScale + 0.5);  // half-grid
+    nPix = 2*nPixHalf + 1;                           // full grid width
+
+    // these are globals used by p_pmAstromGridBin
+    maxOffpix = gridScale * (nPixHalf + 0.5);            // max offset from true center
+    Offset    = maxOffpix / gridScale;
+    Scale     = gridScale;
+
+    // images used as accumulators for the loop below
+    psImage *gridNP = psImageAlloc (nPix, nPix, PS_TYPE_U32);
+    psImage *gridDX = psImageAlloc (nPix, nPix, PS_TYPE_F32);
+    psImage *gridDY = psImageAlloc (nPix, nPix, PS_TYPE_F32);
+    psImage *gridD2 = psImageAlloc (nPix, nPix, PS_TYPE_F32);
+    psImageInit (gridNP, 0);
+    psImageInit (gridDX, 0);
+    psImageInit (gridDY, 0);
+    psImageInit (gridD2, 0);
+
+    // short-cut names for grid images
+    psU32 **NP = gridNP->data.U32;
+    psF32 **DX = gridDX->data.F32;
+    psF32 **DY = gridDY->data.F32;
+    psF32 **D2 = gridD2->data.F32;
+
+    // accumulate grids for focal plane (L,M) matches
+    for (int i = 0; i < raw->n; i++) {
+        ob1 = (pmAstromObj *)raw->data[i];
+        for (int j = 0; j < ref->n; j++) {
+            ob2 = (pmAstromObj *)ref->data[j];
+            dX = ob1->FP->x - ob2->FP->x;
+            dY = ob1->FP->y - ob2->FP->y;
+
+            // fprintf (f, "dX,dY: %8.2f %8.2f : %8.2f %8.2f : %8.2f %8.2f\n", dX, dY, ob1->FP->x, ob2->FP->x, ob1->FP->y, ob2->FP->y);
+            // find bin coordinates for this delta-delta
+            if (!AstromGridBin (&iX, &iY, dX, dY)) {
+                continue; // matched pair is too far offset
+            }
+
+            // accumulate bin stats
+            NP[iY][iX] ++;
+            DX[iY][iX] += dX;
+            DY[iY][iX] += dY;
+            D2[iY][iX] += PS_SQR(dX) + PS_SQR(dY);
+        }
+    }
+
+    // now assess the grid images
+    {
+        double minMetric = 1e10;
+        double minVar = 1e10;
+        int minX = -1;
+        int minY = -1;
+        double metric, var;
+
+        // find the max pixel
+        psStats *imStats = psStatsAlloc (PS_STAT_MAX | PS_STAT_MAX | PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+        if (!psImageStats(imStats, gridNP, NULL, 0)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to get image statistics.\n");
+            psFree(imStats);
+            psFree(gridNP);
+            psFree(gridDX);
+            psFree(gridDY);
+            psFree(gridD2);
+            psFree(stats);
+            return NULL;
+        }
+
+        # if 0
+        char line[16];
+        psFits *fits = psFitsOpen ("grid.image.fits", "w");
+        psFitsWriteImage (fits, NULL, gridNP, 0, NULL);
+        psFitsClose (fits);
+        fprintf (stderr, "wrote grid image, press return to continue\n");
+        fgets (line, 15, stdin);
+        # endif
+
+        // only check bins with at least 1/2 of max bin
+        // XXX requiring at least 3 matches in bin
+        int minNpts = PS_MAX (0.5*imStats->max, 3);
+        psTrace("psModule.astrom.grid.angle", 5, "minNpts: %d, max: %d", minNpts, (int)(imStats->max));
+
+        // find the 'best' bin
+        for (int j = 0; j < gridNP->numRows; j++)
+        {
+            for (int i = 0; i < gridNP->numCols; i++) {
+
+                if (NP[j][i] < minNpts)
+                    continue;
+
+                // this metric emphasizes a narrow peak with lots of sources over one with few.
+                var = fabs((D2[j][i]/NP[j][i]) - PS_SQR(DX[j][i]/NP[j][i]) - PS_SQR(DY[j][i]/NP[j][i]));
+                metric = var / PS_SQR(NP[j][i]) / PS_SQR(NP[j][i]);
+
+                // fprintf (stderr, "try : %f %f (%d pts, %f var, %f met)\n", DX[j][i]/NP[j][i], DY[j][i]/NP[j][i], NP[j][i], var, metric);
+
+                if (metric < minMetric) {
+                    minMetric = metric;
+                    minVar    = var;
+                    minX      = i;
+                    minY      = j;
+                }
+            }
+        }
+
+        // convert the bin to delta-delta
+        if ((minX < 0) || (minY < 0))
+        {
+            // no valid matches found
+            stats->offset.x   = 0;
+            stats->offset.y   = 0;
+            stats->minMetric  = minMetric;
+            stats->minVar     = minVar;
+            stats->nMatch     = 0;
+        } else
+        {
+            stats->offset.x  = DX[minY][minX] / NP[minY][minX];
+            stats->offset.y  = DY[minY][minX] / NP[minY][minX];
+            stats->minMetric = minMetric;
+            stats->minVar    = minVar;
+            stats->nMatch    = NP[minY][minX];
+        }
+
+        psFree (imStats);
+        // XXX EAM : This routine, and pmAstromGridMatch, need to handle failure cases better
+    }
+
+    // sort the NP values and choose
+    psVector *listNP = psVectorAlloc (nPix*nPix, PS_TYPE_U32);
+    int n = 0;
+    for (int i = 0; i < nPix; i++) {
+        for (int j = 0; j < nPix; j++) {
+            listNP->data.U32[n] = gridNP->data.U32[j][i];
+            n++;
+        }
+    }
+    psVector *sort = psVectorSort (NULL, listNP);
+    stats->nTest = sort->data.U32[sort->n - 5];
+    stats->nSigma = (stats->nMatch - stats->nTest) / sqrt(stats->nTest);
+
+    psFree (sort);
+    psFree (listNP);
+    psFree (gridNP);
+    psFree (gridDX);
+    psFree (gridDY);
+    psFree (gridD2);
+    return (stats);
+}
+
+
+
+/******************************************************************************
+pmAstromGridMatch(*raw, *ref, *config): match two star lists.
+ ******************************************************************************/
+
+pmAstromStats *pmAstromGridMatch(const psArray *raw,
+                                 const psArray *ref,
+                                 const psMetadata *config)
+{
+    PS_ASSERT_PTR_NON_NULL(raw, NULL);
+    PS_ASSERT_PTR_NON_NULL(ref, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    bool status;
+    double xMin, xMax, yMin, yMax;
+    const pmAstromObj *obj;
+    psArray *rot;
+
+    pmAstromStats *minStat = pmAstromStatsAlloc ();
+    pmAstromStats *newStat = NULL;
+
+    psPlane center;
+
+    // find center of the raw field (focal-plane coords)
+    xMin = yMin = +1e10;
+    xMax = yMax = -1e10;
+    for (int i = 0; i < raw->n; i++) {
+        obj = (pmAstromObj *)raw->data[i];
+        xMin = PS_MIN (obj->FP->x, xMin);
+        xMax = PS_MAX (obj->FP->x, xMax);
+        yMin = PS_MIN (obj->FP->y, yMin);
+        yMax = PS_MAX (obj->FP->y, yMax);
+    }
+    center.x = 0.5*(xMin + xMax);
+    center.y = 0.5*(yMin + yMax);
+
+    double minAngle = RAD_DEG*psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MIN.ANGLE");
+    double maxAngle = RAD_DEG*psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MAX.ANGLE");
+    double delAngle = RAD_DEG*psMetadataLookupF32 (&status, config, "PSASTRO.GRID.DEL.ANGLE");
+    double minSigma = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MIN.SIGMA");
+
+    minStat->minMetric = 1e10;
+    for (double angle = minAngle; angle <= maxAngle; angle += delAngle) {
+        rot = pmAstromRotateObj (raw, center, angle);
+        newStat = pmAstromGridAngle (rot, ref, config);
+        newStat->angle  = angle;
+        newStat->center = center;
+
+        if (newStat->minMetric < minStat->minMetric) {
+            *minStat = *newStat;
+            psLogMsg ("psModule.astrom", 4, "grid test - offset: %7.2f,%7.2f @ %6.1f deg (%4d pts, %5.1f sig, %5.1f var, %6.3f log metric) *",
+                      minStat->offset.x, minStat->offset.y, DEG_RAD*minStat->angle, minStat->nMatch, minStat->nSigma, minStat->minVar, log10(minStat->minMetric));
+        } else {
+            psLogMsg ("psModule.astrom", 4, "grid test - offset: %7.2f,%7.2f @ %6.1f deg (%4d pts, %5.1f sig, %5.1f var, %6.3f log metric)",
+                      newStat->offset.x, newStat->offset.y, DEG_RAD*newStat->angle, newStat->nMatch, newStat->nSigma, newStat->minVar, log10(newStat->minMetric));
+
+        }
+        psFree (newStat);
+        psFree (rot);
+    }
+    psLogMsg ("psModule.astrom.grid.match", 4, "grid best - offset: %7.2f,%7.2f @ %6.1f deg (%4d pts, %5.1f sig, %5.1f var, %6.3f log metric)",
+              minStat->offset.x, minStat->offset.y, DEG_RAD*minStat->angle, minStat->nMatch, minStat->nSigma, minStat->minVar, log10(minStat->minMetric));
+
+    // I need to decide if a solution is likely to be a good solution or just a mis-match
+    // one posibility: how significant is the peak relative to the 4th or 5th most significant pixel?
+
+    if (minStat->nSigma < minSigma) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to find a valid match (%f sigma for best)", minStat->nSigma);
+        psFree (minStat);
+        return NULL;
+    }
+    return (minStat);
+}
+
+/******************************************************************************
+pmAstromGridTweak(*raw, *ref, *recipe, stats): improve match for two star lists.
+ ******************************************************************************/
+pmAstromStats *pmAstromGridTweak(
+    psArray *raw,
+    psArray *ref,
+    psMetadata *recipe,
+    pmAstromStats *stats)
+{
+    bool status;
+    pmAstromObj *ob1, *ob2;  // short-cut pointers to the objects
+    double dX, dY;   // offset between a possible matched pair
+    psArray *rot;
+    int nBin, xBin, yBin;
+
+    rot = pmAstromRotateObj (raw, stats->center, stats->angle);
+
+    // sampling scale of the grid
+    double tweakScale  = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.SCALE");
+    double tweakRange  = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.RANGE");
+    double tweakSmooth = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.SMOOTH");
+    double tweakNsigma = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.NSIGMA");
+
+    nBin = 2*tweakRange / tweakScale;
+    psVector *xHist = psVectorAlloc (nBin, PS_TYPE_F32);
+    psVector *yHist = psVectorAlloc (nBin, PS_TYPE_F32);
+    psVectorInit (xHist, 0);
+    psVectorInit (yHist, 0);
+
+    // accumulate grids for focal plane (L,M) matches
+    for (int i = 0; i < rot->n; i++) {
+        ob1 = (pmAstromObj *)rot->data[i];
+        for (int j = 0; j < ref->n; j++) {
+            ob2 = (pmAstromObj *)ref->data[j];
+            dX = ob1->FP->x - ob2->FP->x - stats->offset.x;
+            dY = ob1->FP->y - ob2->FP->y - stats->offset.y;
+
+            xBin = (dX + tweakRange) / tweakScale;
+            yBin = (dY + tweakRange) / tweakScale;
+
+            if (xBin < 0)
+                continue;
+            if (yBin < 0)
+                continue;
+            if (xBin >= nBin)
+                continue;
+            if (yBin >= nBin)
+                continue;
+
+            xHist->data.F32[xBin] += 1.0;
+            yHist->data.F32[yBin] += 1.0;
+        }
+    }
+
+    // smooth histgram vector with gaussian of 1sigma = radius
+    psVector *xHistNew = psVectorSmooth(NULL, xHist, tweakSmooth, tweakNsigma);
+    psVector *yHistNew = psVectorSmooth(NULL, yHist, tweakSmooth, tweakNsigma);
+    psFree(xHist);
+    psFree(yHist);
+    xHist = xHistNew;
+    yHist = yHistNew;
+
+    // select peak in x and in y
+    xBin = yBin = 0;
+    double xMax = 0;
+    double yMax = 0;
+    for (int i = 0; i < nBin; i++) {
+        if (xHist->data.F32[i] > xMax) {
+            xBin = i;
+            xMax = xHist->data.F32[i];
+        }
+        if (yHist->data.F32[i] > yMax) {
+            yBin = i;
+            yMax = yHist->data.F32[i];
+        }
+    }
+    double xPeak = xBin*tweakScale - tweakRange;
+    double yPeak = yBin*tweakScale - tweakRange;
+    psLogMsg (__func__, 3, "tweak peak by %f,%f\n", xPeak, yPeak);
+
+    // adjust offset by peak center
+    pmAstromStats *tweak = pmAstromStatsAlloc();
+    *tweak = *stats;
+    tweak->offset.x += xPeak;
+    tweak->offset.y += yPeak;
+
+    psFree (rot);
+    psFree (xHist);
+    psFree (yHist);
+
+    return tweak;
+}
+
+/******************************************************************************
+pmAstromGridApply(*map, stat): apply the measured FPA offset and rotation
+(stat) to the fpa astrom structures.
+ ******************************************************************************/
+psPlaneTransform *pmAstromGridApply(
+    psPlaneTransform *map,
+    pmAstromStats *stat)
+{
+    PS_ASSERT_PTR_NON_NULL(map, NULL);
+    PS_ASSERT_POLY_NON_NULL(map->x, NULL);
+    PS_ASSERT_POLY_NON_NULL(map->y, NULL);
+
+    double cs = cos (stat->angle);
+    double sn = sin (stat->angle);
+
+    double dx = (map->x->coeff[0][0] - stat->center.x);
+    double dy = (map->y->coeff[0][0] - stat->center.y);
+
+    // new offset
+    map->x->coeff[0][0] =  cs*dx + sn*dy - stat->offset.x + stat->center.x;
+    map->y->coeff[0][0] = -sn*dx + cs*dy - stat->offset.y + stat->center.y;
+
+    // original rotation matrix
+    double pc1_1 = map->x->coeff[1][0];
+    double pc1_2 = map->x->coeff[0][1];
+    double pc2_1 = map->y->coeff[1][0];
+    double pc2_2 = map->y->coeff[0][1];
+
+    // new rotation matrix
+    map->x->coeff[1][0] = +cs*pc1_1 + sn*pc2_1;
+    map->x->coeff[0][1] = +cs*pc1_2 + sn*pc2_2;
+    map->y->coeff[1][0] = -sn*pc1_1 + cs*pc2_1;
+    map->y->coeff[0][1] = -sn*pc1_2 + cs*pc2_2;
+
+    return (map);
+}
+
+/* Illustration of the grid bins
+   dX        bin
+   -35:-25 -> 0     bin = dX / Scale + Offset
+   -25:-15 -> 1     Scale = 10
+   -15:-05 -> 2     Offset = 3.5
+   -05:+05 -> 3     nPix = 3 (maxOffset = 35 = (nPix + 0.5)*dXix
+   +05:+15 -> 4     dPix = 10
+   +15:+25 -> 5
+   +25:+35 -> 6
+
+   maxOffsetRequest = 30
+   nPix = (int) (maxOffset / dPix + 0.5);
+   maxOffset = (nPix + 0.5)*Scale;
+*/
+
Index: /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryObjects.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryObjects.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryObjects.h	(revision 15358)
@@ -0,0 +1,346 @@
+/* @file  pmAstrometryObjects.h
+ * @brief basic matching of objects based on their astrometry.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.16 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-08-23 23:44:05 $
+ * Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_OBJECTS_H
+#define PM_ASTROMETRY_OBJECTS_H
+
+/// @addtogroup Astrometry
+/// @{
+
+/*
+ *
+ * This structure specifies the coordinate of the detection in each of the
+ * four necessary coordinate frames: pix defines the position in the psReadout
+ * frame, FP defines the position in the Focal Plane frame, TP defines the
+ * position in the Tangent Plane frame, sky defines the position on the Celestial
+ * Sphere. In addition, a measurement of the brightness is given by the element
+ * Mag. Such a data structure should be used for both the raw and the reference
+ * stars. In astrometric processing, the raw detections will be projected using
+ * the best available information to each of these coordinate frames from the pix
+ * coordinates, while the reference detections will be projected to the other
+ * frames from the sky coordinates.
+ *
+ * XXX: There are more members here than in the SDRS.
+ *
+ */
+typedef struct
+{
+    psPlane *pix;   ///< the position in the pmReadout frame
+    psPlane *cell;   ///< the position in the pmCell frame
+    psPlane *chip;   ///< the position in the pmChip frame
+    psPlane *FP;   ///< the position in the pmFPA frame
+    psPlane *TP;   ///< the position in the tangent plane
+    psSphere *sky;        ///< the position on the Celestial Sphere.
+    double Mag;                         ///< object magnitude XXX what filter?
+    double dMag;                        ///< error on object magnitude
+}
+pmAstromObj;
+
+/*
+ *
+ * The pmAstromMatch structure defines the cross-correlation between two
+ * arrays. A single such data item specifies that item number pmAstromMatch.idx1
+ * in the first list corresponds to pmAstromMatch.idx2 in the second list.
+ *
+ */
+typedef struct
+{
+    int raw;                             ///< What is this?
+    int ref;                             ///< What is this?
+}
+pmAstromMatch;
+
+
+/*
+ *
+ * XXX: Not in SDRS.
+ *
+ */
+typedef struct
+{
+    psPlane center;                     ///<
+    psPlane offset;                     ///<
+    double  angle;                      ///<
+    double  minMetric;                  ///<
+    double  minVar;                     ///<
+    int     nMatch;                     ///<
+    int     nTest;                      ///<
+    double  nSigma;                     ///<
+}
+pmAstromStats;
+
+typedef struct
+{
+    psStats *xStats;
+    psStats *yStats;
+    int     nMatch;                     ///<
+    double  nSigma;                     ///<
+}
+pmAstromFitResults;
+
+/*
+ *
+ * If the two sets of coordinates are expected to agree very well (ie, the current best-guess
+ * astrometric solution is quite close to reality), perform a match based on a simple radius
+ * test. The following functions accept two sets of pmAstromObj sources and determines the
+ * matched objects between the two lists using coordinates of the desired depth (depending on
+ * the function). The input and reference sources must have been projected to the desired depth
+ * (eg, for Focal Plane coordinates, to pmAstromObj.FP).  The specified radius must be in the
+ * units for the matching depth (chip: pixels, focal plane: microns, tangent plane:
+ * degrees. The output consists an array of pmAstromMatch values, defined above.
+ *
+ */
+psArray *pmAstromRadiusMatch(
+    const psArray *st1,
+    const psArray *st2,
+    double RADIUS
+);
+psArray *pmAstromRadiusMatchFP(
+    const psArray *st1,
+    const psArray *st2,
+    double RADIUS
+);
+psArray *pmAstromRadiusMatchTP(
+    const psArray *st1,
+    const psArray *st2,
+    double RADIUS
+);
+psArray *pmAstromRadiusMatchChip(
+    const psArray *st1,
+    const psArray *st2,
+    double RADIUS
+);
+
+
+pmAstromStats *pmAstromStatsAlloc(void);
+
+/*
+ *
+ * This function accepts an array of pmAstromObj objects and rotates them by
+ * the given angle about the given center coordinate pCenter,qCenter in the Focal
+ * Plane Array coordinates.
+ *
+ * XXX: This differs from the SDRS
+ *
+ */
+/* SDRS
+psArray *pmAstromRotateObj(
+    psArray *old,
+    double angle,
+    double pCenter,
+    double qCenter
+);
+*/
+psArray *pmAstromRotateObj(
+    const psArray *old,
+    psPlane center,
+    double angle
+);
+
+
+/*
+ *
+ * If the two sets of coordinates are not known to agree well, but the
+ * relative scale and approximate relative rotation is known, then a much faster
+ * match can be found using pair-pair displacements. In such a case, the two
+ * lists can be considered as having the same coordinate system, with an unknown
+ * relative displacement. In this algorithm, all possible pair-wise differences
+ * between the source positions in the two lists are constructed and accumulated
+ * in a grid of possible offset values. The resulting grid is searched for a
+ * cluster representing the offset between the two input lists. This algorithm
+ * can only tolerate a small error in the relative scale or the relative rotation
+ * of the two coordinate lists. However, this process is naturally O(N2), and is
+ * thus advantageous over triangle matching in some circumstances. This process
+ * can be extended to allow a larger uncertainty in the relative rotation by
+ * allowing the procedure to scan over a range of rotations. We define the
+ * following function to apply this matching algorithm:
+ *
+ * XXX: In the SDRS, this function is a pointer.
+ *
+ */
+pmAstromStats *pmAstromGridMatch(
+    const psArray *st1,
+    const psArray *st2,
+    const psMetadata *config
+);
+
+/******************************************************************************
+pmAstromGridTweak(*raw, *ref, *recipe, stats): improve match for two star lists.
+ ******************************************************************************/
+pmAstromStats *pmAstromGridTweak(
+    psArray *raw,
+    psArray *ref,
+    psMetadata *recipe,
+    pmAstromStats *stats);
+
+/*
+ *
+ * The result of a pmAstromGridMatch may be used to modify the astrometry
+ * transformation information for a pmFPA image hierarchy structure. The result
+ * of pmAstromGridMatch defines the adjustments which should be made to the
+ * reference coordinate of the projection (pmFPA.projection.R,D) and the
+ * effective rotation of the Focal Plane.  The rotation implies modification of
+ * the linear terms of the pmFPA.toTangentPlane transformation. These two
+ * adjustments are made using the function:
+ *
+ * XXX: This function name is different in the SDRS.
+ *
+ */
+psPlaneTransform *pmAstromGridApply(
+    psPlaneTransform *map,
+    pmAstromStats *stat
+);
+
+
+/*
+ *
+ * This function is identical to pmAstromGridMatch, but is valid for only a
+ * single relative rotation. The input config information need not contain any of
+ * the GRID.*.ANGLE entries (they will be ignored).
+ *
+ * XXX: This function name is different in the SDRS.
+ *
+ */
+/* in pmAstromGrid.c */
+pmAstromStats *pmAstromGridAngle(
+    const psArray *st1,
+    const psArray *st2,
+    const psMetadata *config);
+
+
+
+/*
+ *
+ * This function accepts the raw and reference source lists and the list of
+ * matched entries. It uses the matched list to determine a polynomial
+ * transformation between the two coordinate systems. The fitting uses clipping
+ * to exclude outliers, likely representing poor matches. The config element must
+ * contain the information ASTROM.NSIGMA (specifying the number of sigma used in
+ * the clipping) and ASTROM.NCLIP (specifying the number of clipping iterations
+ * must be performed). The config element must also specify the order of the
+ * polynomial fit (keyword: ASTROM.ORDER). The result of this fit is a set of
+ * modifications of the components of the pmFPA.toTangentPlane transformation,
+ * and the modifications of the reference coordinate of the projection
+ * (pmFPA.projection.R,D) and the projection scale (pmFPA.projection.Xs,Ys). The
+ * modifications to pmFPA.toTangentPlane incorporate the rotation component of
+ * the linear terms and the higher-order terms of the polynomial fits.
+ *
+ * XXX: No prototype code.
+ *
+ */
+bool pmAstromFitFPA(
+    pmFPA *fpa,
+    psArray *st1,
+    psArray *st2,
+    psArray *match,
+    psMetadata *config
+);
+
+
+
+/*
+ *
+ * This function accepts the raw and reference source lists for a single chip
+ * and the list of matched entries. It uses the matched list to determine a
+ * polynomial transformation between the two coordinate systems. The fitting
+ * uses clipping to exclude outliers, likely representing poor matches. The
+ * config element must contain the information ASTROM.NSIGMA
+ *(specifying the number of sigma used in the clipping) and ASTROM.NCLIP
+ *(specifying the number of clipping iterations must be performed). The config
+ *element must also specify the order of the polynomial fit (keyword:
+ *ASTROM.ORDER).  The result of this fit is a set of modifications of the
+ *components of the pmChip.toFPA transformation.
+ *
+ * XXX: No prototype code.
+ *
+ */
+bool pmAstromFitChip(
+    pmFPA *fpa,
+    psArray *st1,
+    psArray *st2,
+    psArray *match,
+    psMetadata *config
+);
+
+
+/*******************************************************************************
+ The following functions and structs were in the prototype code, but not the
+ SDRS.
+ ******************************************************************************/
+/*
+ *
+ *
+ *
+ *
+ */
+
+pmAstromFitResults *pmAstromFitResultsAlloc(void);
+
+/*
+ *
+ * Allocates a pmAstromObj struct.
+ *
+ */
+pmAstromObj *pmAstromObjAlloc (void);
+/*
+ * Is a given pointer a pmAstromObj?
+ */
+bool pmAstromObjTest(const psPtr ptr);
+
+
+/*
+ *
+ * Copies a pmAstromObj struct.
+ *
+ */
+pmAstromObj *pmAstromObjCopy(
+    const pmAstromObj *old
+);
+
+
+
+/*
+ *
+ *
+ *
+ */
+pmAstromMatch *pmAstromMatchAlloc(
+    int i1,
+    int i2
+);
+
+
+
+
+/*
+ *
+ *
+ *
+ */
+pmAstromFitResults *pmAstromMatchFit(
+    psPlaneTransform *map,
+    psArray *raw,
+    psArray *ref,
+    psArray *match,
+    psStats *stats
+);
+
+/*
+ *
+ *
+ *
+ */
+int pmAstromObjSortByMag(
+    const void **a,
+    const void **b
+);
+
+/// @}
+#endif // PM_ASTROMETRY_OBJECTS_H
Index: /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryRegions.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryRegions.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryRegions.c	(revision 15358)
@@ -0,0 +1,168 @@
+/** @file  pmAstrometryRegions.c
+ *  @brief functions to define astrometry regions on FPA images
+ *  @ingroup Astrometry
+ *
+ *  @author EAM, IfA
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-04-01 03:10:12 $
+ *
+ *  Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAExtent.h"
+#include "pmAstrometryRegions.h"
+
+// cell pixels corresponding to readout boundary
+psRegion *pmAstromReadoutInCell (pmReadout *readout) {
+
+    psRegion *region;
+    region = pmReadoutExtent (readout);
+    return (region);
+}
+
+// chip pixels corresponding to cell boundary
+psRegion *pmAstromCellInChip (pmCell *cell) {
+
+    psRegion *region;
+    region = pmCellExtent (cell);
+    return (region);
+}
+
+// FP pixels corresponding to chip boundary
+// since the chip may be rotated in the fpa, this region does not correspond 
+// exactly to the pixel grid of the chip
+psRegion *pmAstromChipInFP (pmChip *chip) {
+
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip->toFPA, NULL);
+
+    // determine the bounding box of this chip in chip pixels
+    psRegion *chipExtent = pmChipPixels (chip);
+    if (!chipExtent) return NULL;
+
+    // apply chip-to-fpa astrometry to determine fpa coordinates 
+    psPlane *chPix = psPlaneAlloc ();
+    psPlane *fpPix = psPlaneAlloc ();
+    psRegion *chipRegion = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of chip
+
+    // determine the outer bounding box -- does not correspond to the same square region!
+    chPix->x = chipExtent->x0;
+    chPix->y = chipExtent->y0;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    chPix->x = chipExtent->x1;
+    chPix->y = chipExtent->y0;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    chPix->x = chipExtent->x1;
+    chPix->y = chipExtent->y1;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    chPix->x = chipExtent->x0;
+    chPix->y = chipExtent->y1;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    psFree (chPix);
+    psFree (fpPix);
+    psFree (chipExtent);
+    return (chipRegion);
+}
+
+// return FPA pixels included in all chips
+// this FPA grid has 0,0 at the mosaic center and is used for astrometric reference.
+psRegion *pmAstromFPAExtent(const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    psArray *chips = fpa->chips;       // Array of component chips
+    psRegion *fpaExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of fpa
+    for (long i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        psRegion *chipExtent = pmAstromChipInFP(chip); // Extent of chip
+	if (!chipExtent) { continue; }
+        fpaExtent->x0 = PS_MIN(fpaExtent->x0, chipExtent->x0);
+        fpaExtent->x1 = PS_MAX(fpaExtent->x1, chipExtent->x1);
+        fpaExtent->y0 = PS_MIN(fpaExtent->y0, chipExtent->y0);
+        fpaExtent->y1 = PS_MAX(fpaExtent->y1, chipExtent->y1);
+        psFree(chipExtent);
+    }
+
+    return fpaExtent;
+}
+
+// TPA pixels corresponding to FPA boundary
+psRegion *pmAstromFPInTP (pmFPA *fpa) {
+
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa->toTPA, NULL);
+
+    psRegion *fpaExtent = pmAstromFPAExtent (fpa);
+    if (!fpaExtent) return NULL;
+
+    // apply fpa-to-tpa astrometry to determine tpa coordinates 
+    psPlane *fpPix = psPlaneAlloc ();
+    psPlane *tpPix = psPlaneAlloc ();
+    psRegion *fpaRegion = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of fpa
+
+    // determine the outer bounding box -- does not correspond to the same square region!
+    fpPix->x = fpaExtent->x0;
+    fpPix->y = fpaExtent->y0;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    fpPix->x = fpaExtent->x1;
+    fpPix->y = fpaExtent->y0;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    fpPix->x = fpaExtent->x1;
+    fpPix->y = fpaExtent->y1;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    fpPix->x = fpaExtent->x0;
+    fpPix->y = fpaExtent->y1;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    psFree (fpPix);
+    psFree (tpPix);
+    psFree (fpaExtent);
+    return (fpaRegion);
+}
Index: /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryRegions.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryRegions.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryRegions.h	(revision 15358)
@@ -0,0 +1,35 @@
+/* @file  pmAstrometryRegion.h
+ * @brief functions to detemine fpa,chip,etc boundaries from astrometry
+ *
+ * @author EAM, IfA
+ * @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-21 21:59:57 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_REGIONS_H
+#define PM_ASTROMETRY_REGIONS_H
+
+/// @addtogroup Astrometry
+/// @{
+
+// cell pixels corresponding to readout boundary
+psRegion *pmAstromReadoutInCell (pmReadout *readout);
+
+// chip pixels corresponding to cell boundary
+psRegion *pmAstromCellInChip (pmCell *cell);
+
+// FP pixels corresponding to chip boundary
+// since the chip may be rotated in the fpa, this region does not correspond 
+// exactly to the pixel grid of the chip
+psRegion *pmAstromChipInFP (pmChip *chip);
+
+// return FPA pixels included in all chips
+// this FPA grid has 0,0 at the mosaic center and is used for astrometric reference.
+psRegion *pmAstromFPAExtent(const pmFPA *fpa);
+
+// chip pixels corresponding to cell boundary
+psRegion *pmAstromFPInTP (pmFPA *fpa);
+
+/// @}
+#endif // PM_ASTROMETRY_DISTORTION_H
Index: /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryTable.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryTable.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryTable.c	(revision 15358)
@@ -0,0 +1,272 @@
+/** @file  pmAstrometryObjects.c
+*
+*  @brief This file defines the basic types for matching objects
+*  based on their astrometry.
+*
+*  @ingroup AstroImage
+*
+*  @author EAM, IfA
+*
+*  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2007-10-23 20:52:42 $
+*
+*  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+*/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+/******************************************************************************/
+/*  INCLUDE FILES                                                             */
+/******************************************************************************/
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <unistd.h>   // for unlink
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmAstrometryObjects.h"
+
+/********************* CheckDataStatus functions *****************************/
+
+bool pmAstromCheckDataStatusForView (const pmFPAview *view, pmFPAfile *file) {
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        bool exists = pmAstromCheckDataStatusForFPA (fpa);
+        return exists;
+    }
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        bool exists = pmAstromCheckDataStatusForChip (chip);
+        return exists;
+    }
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
+        return false;
+    }
+    psError(PS_ERR_IO, false, "Astrometry only valid at the chip level");
+    return false;
+}
+
+bool pmAstromCheckDataStatusForFPA (const pmFPA *fpa) {
+
+    if (!fpa->toTPA) return false;
+    if (!fpa->fromTPA) return false;
+    if (!fpa->toSky) return false;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        if (pmAstromCheckDataStatusForChip (chip)) return true;
+    }
+    return false;
+}
+
+bool pmAstromCheckDataStatusForChip (const pmChip *chip) {
+
+    if (!chip->toFPA) return false;
+    if (!chip->fromFPA) return false;  // XXX not strictly needed?
+    return true;
+}
+
+/********************* WritePHU function *****************************/
+
+bool pmAstromWritePHU (const pmFPAview *view, pmFPAfile *file, const pmConfig *config) { 
+
+    // not needed if already written
+    if (file->wrote_phu) return true;
+
+    // not needed if not FPA
+    // XXX this prevents us from defining a SPLIT/MEF CMF file...
+    if (file->fileLevel != PM_FPA_LEVEL_FPA) return true;
+
+    // find the FPA phu
+    // XXX do we need to supply a header from the image?
+    pmHDU *phu = pmFPAviewThisPHU (view, file->fpa);
+
+    // if there is no PHU, this is a single header+image (extension-less) file. This could be
+    // the case for an input SPLIT set of files being written out as a MEF.  if there is a PHU,
+    // write it out as a 'blank'
+    psMetadata *outhead = psMetadataAlloc();
+    if (phu) {
+        psMetadataCopy (outhead, phu->header);
+    } else {
+        pmConfigConformHeader (outhead, file->format);
+
+	// XXX probably need other info to conform the header
+        psMetadata *fileData = psMetadataLookupMetadata(NULL, file->format, "FILE"); // File information
+        const char *fpaNameHdr = psMetadataLookupStr(NULL, fileData, "FPA.NAME");
+        if (fpaNameHdr && strlen(fpaNameHdr) > 0) {
+            const char *fpaName = psMetadataLookupStr(NULL, file->fpa->concepts, "FPA.NAME");
+            psMetadataAddStr(outhead, PS_LIST_TAIL, fpaNameHdr, PS_META_REPLACE, "FPA name", fpaName);
+        }
+    }
+
+    psMetadataAddBool (outhead, PS_LIST_TAIL, "EXTEND", PS_META_REPLACE, "this file has extensions", true);
+    psFitsWriteBlank (file->fits, outhead, "");
+    file->wrote_phu = true;
+
+    psTrace ("pmFPAfile", 5, "wrote phu %s (type: %d)\n", file->filename, file->type);
+    psFree (outhead);
+
+    return true;
+}
+
+/********************* Write Data functions *****************************/
+
+bool pmAstromWriteForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        if (!pmAstromWriteFPA (fpa, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write Astrometry for fpa");
+            return false;
+        }
+        return true;
+    }
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        if (!pmAstromWriteChip (chip, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write Astrometry for chip");
+            return false;
+        }
+        return true;
+    }
+
+    psError(PS_ERR_IO, false, "Astrometry must be written at the chip level");
+    return false;
+}
+
+// write out all chip-level Astrometry data for this FPA
+bool pmAstromWriteFPA (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        if (!pmAstromWriteChip (chip, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write Astrometry for %dth chip", i);
+            psFree(thisView);
+            return false;
+        }
+    }
+    psFree(thisView);
+    return true;
+}
+
+// write out chip-level Astrometry data for this chip
+// XXX we need to ensure the chip and fpa level data are written
+// XXX can we write the rows one at a time (or groups at a time?)
+bool pmAstromWriteChip (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    // define the EXTNAME values used for the table data, and residual image segments
+    {
+        // lookup the EXTNAME values used for table data and image header segments
+        char *rule = NULL;
+
+        // Menu of EXTNAME rules
+        psMetadata *menu = psMetadataLookupMetadata(&status, file->camera, "EXTNAME.RULES");
+        if (!menu) {
+            psError(PS_ERR_UNKNOWN, true, "missing EXTNAME.RULES in camera.config");
+            return false;
+        }
+
+        // EXTNAME for image header
+        rule = psMetadataLookupStr(&status, menu, "ASTROMETRY");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, false, "missing entry for ASTROMETRY in EXTNAME.RULES in camera.config");
+            return false;
+        }
+        extname = pmFPAfileNameFromRule (rule, file, view);
+    }
+
+    // write the chip elements in the following form:
+    // chipID, ...?  I forget the form: look at ICD
+
+    return true;
+}
+
+/********************* Read Data functions *****************************/
+
+bool pmAstromReadForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        return pmAstromReadFPA(fpa, view, file, config);
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        psAbort("Programming error: view does not apply to FPA.");
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        return pmAstromReadChip(chip, view, file, config);
+    }
+
+    psError(PS_ERR_IO, false, "Astrometry must be read at the chip level");
+    return false;
+}
+
+// read in fpa-level Astrometry files for this FPA, and all chips
+bool pmAstromReadFPA (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    bool success = true;                // Was everything successful?
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        success &= pmAstromReadChip(chip, view, file, config);
+    }
+    return success;
+}
+
+// read in chip-level Astrometry data for this chip
+bool pmAstromReadChip (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    // define the EXTNAME values used for the table data, and residual image segments
+    {
+        // lookup the EXTNAME values used for table data and image header segments
+        char *rule = NULL;
+
+        // Menu of EXTNAME rules
+        psMetadata *menu = psMetadataLookupMetadata(&status, file->camera, "EXTNAME.RULES");
+        if (!menu) {
+            psError(PS_ERR_UNKNOWN, true, "missing EXTNAME.RULES in camera.config");
+            return false;
+        }
+
+        // EXTNAME for image header
+        rule = psMetadataLookupStr(&status, menu, "ASTROMETRY");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, false, "missing entry for ASTROMETRY in EXTNAME.RULES in camera.config");
+            return false;
+        }
+        extname = pmFPAfileNameFromRule (rule, file, view);
+    }
+
+    // read the chip elements in the following form:
+    // chipID, ...?  I forget the form: look at ICD
+
+    return true;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryUtils.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryUtils.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryUtils.c	(revision 15358)
@@ -0,0 +1,341 @@
+/** @file  pmAstrometryUtils.c
+ *
+ *  @brief utility functions for transform and distort functions
+ *
+ *  @ingroup Astrometry
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-10-09 19:27:04 $
+ *
+ *  Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmAstrometryUtils.h"
+
+// given a 2D transformation -- L(x,y),M(x,y) -- find the coordinates x,y
+// for which L,M = 0,0. tol is the allowed error on x,y.
+psPlane *psPlaneTransformGetCenter (psPlaneTransform *trans, double tol)
+{
+
+    // crpix1,2 = X,Y(crval1,2)
+    // start with linear solution for Xo,Yo:
+    double R  = (trans->x->coeff[1][0]*trans->y->coeff[0][1] - trans->x->coeff[0][1]*trans->y->coeff[1][0]);
+    double Xo = (trans->y->coeff[0][0]*trans->x->coeff[0][1] - trans->x->coeff[0][0]*trans->y->coeff[0][1])/R;
+    double Yo = (trans->x->coeff[0][0]*trans->y->coeff[1][0] - trans->y->coeff[0][0]*trans->x->coeff[1][0])/R;
+
+    // iterate to actual solution: requires small non-linear terms
+    if (trans->x->nX > 1) {
+        psPolynomial2D *XdX = psPolynomial2D_dX(NULL, trans->x);
+        psPolynomial2D *XdY = psPolynomial2D_dY(NULL, trans->x);
+
+        psPolynomial2D *YdX = psPolynomial2D_dX(NULL, trans->y);
+        psPolynomial2D *YdY = psPolynomial2D_dY(NULL, trans->y);
+
+        psImage *Alpha = psImageAlloc (2, 2, PS_DATA_F32);
+        psVector *Beta = psVectorAlloc (2, PS_DATA_F32);
+
+        /* this loop uses the Newton-Raphson method to solve for Xo,Yo
+        * it needs the high order terms to be small 
+        * Xo,Yo are in pixels;
+        */
+        double dPos = tol + 1;
+        for (int i = 0; (dPos > tol) && (i < 20); i++) {
+            // NOTE: order for Alpha is: [y][x]
+            Alpha->data.F32[0][0] = psPolynomial2DEval (XdX, Xo, Yo);
+            Alpha->data.F32[1][0] = psPolynomial2DEval (XdY, Xo, Yo);
+            Alpha->data.F32[0][1] = psPolynomial2DEval (YdX, Xo, Yo);
+            Alpha->data.F32[1][1] = psPolynomial2DEval (YdY, Xo, Yo);
+
+            Beta->data.F32[0] = psPolynomial2DEval (trans->x, Xo, Yo);
+            Beta->data.F32[1] = psPolynomial2DEval (trans->y, Xo, Yo);
+
+            psMatrixGJSolveF32 (Alpha, Beta);
+
+            Xo -= Beta->data.F32[0];
+            Yo -= Beta->data.F32[1];
+            dPos = hypot(Beta->data.F32[0], Beta->data.F32[1]);
+        }
+        psFree (Alpha);
+        psFree (Beta);
+        psFree (XdX);
+        psFree (XdY);
+        psFree (YdX);
+        psFree (YdY);
+    }
+    psPlane *center = psPlaneAlloc ();
+    center->x = Xo;
+    center->y = Yo;
+
+    return center;
+}
+
+// convert a transformation L(x,y) to L'(x-xo,y-yo)
+psPlaneTransform *psPlaneTransformSetCenter (psPlaneTransform *output, psPlaneTransform *input, double Xo, double Yo)
+{
+
+    // validate fit order
+
+    if (output == NULL) {
+        output = psPlaneTransformAlloc(input->x->nX, input->x->nY);
+    }
+
+    /* given two equivalent polynomial representations L(x,y) = \sum_i \sum_j A_{i,j} x^i y^j
+     * we can transform L(x,y) into L'(x-xo,y-yo) by taking the derivatives of both sides and 
+     * noting that the constant term in each is the coefficient in the case of L(x,y) and is the 
+     * value of L'(-xo,-yo) in the second case.
+     */
+
+    psPolynomial2D *tmp;
+
+    psPolynomial2D *xPx = psPolynomial2DCopy (NULL, input->x);
+    psPolynomial2D *yPx = psPolynomial2DCopy (NULL, input->y);
+
+    for (int i = 0; i <= input->x->nX; i++) {
+        psPolynomial2D *xPy = psPolynomial2DCopy (NULL, xPx);
+        psPolynomial2D *yPy = psPolynomial2DCopy (NULL, yPx);
+        for (int j = 0; j <= input->x->nY; j++) {
+            output->x->coeffMask[i][j] = input->x->coeffMask[i][j];
+            output->y->coeffMask[i][j] = input->y->coeffMask[i][j];
+            output->x->coeff[i][j] = (output->x->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0 : psPolynomial2DEval (xPy, Xo, Yo) / tgamma(i+1) / tgamma(j+1);
+            output->y->coeff[i][j] = (output->y->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0 : psPolynomial2DEval (yPy, Xo, Yo) / tgamma(i+1) / tgamma(j+1);
+
+            // take the next derivative wrt y, catch output (is NULL on last pass)
+            tmp = psPolynomial2D_dY(NULL, xPy);
+            psFree (xPy);
+            xPy = tmp;
+            tmp = psPolynomial2D_dY(NULL, yPy);
+            psFree (yPy);
+            yPy = tmp;
+        }
+        // take the next derivative wrt x, catch output (is NULL on last pass)
+        tmp = psPolynomial2D_dX(NULL, xPx);
+        psFree (xPx);
+        xPx = tmp;
+        tmp = psPolynomial2D_dX(NULL, yPx);
+        psFree (yPx);
+        yPx = tmp;
+    }
+    return output;
+}
+
+// construct a psPlaneTransform which is the identify transformation
+psPlaneTransform *psPlaneTransformIdentity (int order)
+{
+
+    psPlaneTransform *transform;
+
+    if (order < 1)
+        psAbort("invalid order");
+    if (order > 3)
+        psAbort("invalid order");
+
+    // all coeffs and masks initially set to 0
+    transform = psPlaneTransformAlloc (order, order);
+
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                transform->x->coeffMask [i][j] = PS_POLY_MASK_SET;
+                transform->y->coeffMask [i][j] = PS_POLY_MASK_SET;
+            }
+        }
+    }
+    transform->x->coeff[1][0] = 1;
+    transform->y->coeff[0][1] = 1;
+    transform->x->coeffMask[1][0] = PS_POLY_MASK_NONE;
+    transform->y->coeffMask[0][1] = PS_POLY_MASK_NONE;
+
+    return transform;
+}
+
+// check that the given psPlaneTransform is the identity * (Xs,Ys)
+bool psPlaneTransformIsDiagonal (psPlaneTransform *transform)
+{
+
+    int order;
+    bool status;
+
+    // we currently only support up to 3rd order polynomials
+    if (transform->x->nX < 1)
+        return false;
+    if (transform->x->nY < 1)
+        return false;
+    if (transform->y->nX < 1)
+        return false;
+    if (transform->y->nY < 1)
+        return false;
+
+    if (transform->x->nX != transform->x->nY)
+        return false;
+    if (transform->y->nX != transform->y->nY)
+        return false;
+
+    // these are not actually valid tests
+    if (transform->x->nX > 3)
+        return false;
+    if (transform->x->nY > 3)
+        return false;
+    if (transform->y->nX > 3)
+        return false;
+    if (transform->y->nY > 3)
+        return false;
+
+    status = true;
+    order = transform->x->nX;
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                // high-order cross terms must be masked (eg, x^3 y^2)
+                status &= (transform->x->coeffMask[i][j] & PS_POLY_MASK_SET);
+            } else {
+                status &= !(transform->x->coeffMask[i][j] & PS_POLY_MASK_SET);
+                if ((i == 1) && (i + j == 1)) {
+                    // linear, diagonal terms must be non-zero
+                    status &= (fabs(transform->x->coeff[i][j]) > FLT_EPSILON);
+                } else {
+                    // non-linear and off-diagonal terms must be 0 (eg, x^2, x y)
+                    status &= (fabs(transform->x->coeff[i][j]) < FLT_EPSILON);
+                }
+            }
+        }
+    }
+
+    order = transform->y->nX;
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                // high-order cross terms must be masked (eg, x^3 y^2)
+                status &= (transform->y->coeffMask[i][j] & PS_POLY_MASK_SET);
+            } else {
+                status &= !(transform->y->coeffMask[i][j] & PS_POLY_MASK_SET);
+                if ((j == 1) && (i + j == 1)) {
+                    // linear, diagonal terms must be 1.0
+                    status &= (fabs(transform->y->coeff[i][j]) > FLT_EPSILON);
+                } else {
+                    // non-linear and off-diagonal terms must be 0 (eg, x^2, x y)
+                    status &= (fabs(transform->y->coeff[i][j]) < FLT_EPSILON);
+                }
+            }
+        }
+    }
+    return status;
+}
+
+// construct a psPlaneDistort which is the identify transformation
+psPlaneDistort *psPlaneDistortIdentity (int order)
+{
+
+    psPlaneDistort *distort;
+
+    if (order < 1)
+        psAbort("invalid order");
+    if (order > 3)
+        psAbort("invalid order");
+
+    // all coeffs and masks initially set to 0
+    distort = psPlaneDistortAlloc (order, order, 0, 0);
+
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                distort->x->coeffMask [i][j][0][0] = PS_POLY_MASK_SET;
+                distort->y->coeffMask [i][j][0][0] = PS_POLY_MASK_SET;
+            }
+        }
+    }
+    distort->x->coeff[1][0][0][0] = 1;
+    distort->y->coeff[0][1][0][0] = 1;
+
+    return distort;
+}
+
+// check that the given psPlaneDistort is the identity * (Xs,Ys)
+bool psPlaneDistortIsDiagonal (psPlaneDistort *distort)
+{
+
+    int order;
+    bool status;
+
+    // we currently only support up to 3rd order polynomials
+    if (distort->x->nX < 1)
+        return false;
+    if (distort->x->nY < 1)
+        return false;
+    if (distort->y->nX < 1)
+        return false;
+    if (distort->y->nY < 1)
+        return false;
+
+    if (distort->x->nX > 3)
+        return false;
+    if (distort->x->nY > 3)
+        return false;
+    if (distort->y->nX > 3)
+        return false;
+    if (distort->y->nY > 3)
+        return false;
+
+    if (distort->x->nZ > 0)
+        return false;
+    if (distort->x->nT > 0)
+        return false;
+    if (distort->y->nZ > 0)
+        return false;
+    if (distort->y->nT > 0)
+        return false;
+
+    if (distort->x->nX != distort->x->nY)
+        return false;
+    if (distort->y->nX != distort->y->nY)
+        return false;
+
+    status = true;
+    order = distort->x->nX;
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                // high-order cross terms must be masked (eg, x^3 y^2)
+                status &= (distort->x->coeffMask[i][j][0][0] & PS_POLY_MASK_SET);
+            } else {
+                status &= !(distort->x->coeffMask[i][j][0][0] & PS_POLY_MASK_SET);
+                if ((i == 1) && (i + j == 1)) {
+                    // linear, diagonal terms must be 1.0
+                    status &= (fabs(distort->x->coeff[i][j][0][0]) > FLT_EPSILON);
+                } else {
+                    // non-linear and off-diagonal terms must be 0 (eg, x^2, x y)
+                    status &= (fabs(distort->x->coeff[i][j][0][0]) < FLT_EPSILON);
+                }
+            }
+        }
+    }
+
+    order = distort->y->nX;
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                // high-order cross terms must be masked (eg, x^3 y^2)
+                status &= (distort->y->coeffMask[i][j][0][0] & PS_POLY_MASK_SET);
+            } else {
+                status &= !(distort->y->coeffMask[i][j][0][0] & PS_POLY_MASK_SET);
+                if ((j == 1) && (i + j == 1)) {
+                    // linear, diagonal terms must be 1.0
+                    status &= (fabs(distort->y->coeff[i][j][0][0]) > FLT_EPSILON);
+                } else {
+                    // non-linear and off-diagonal terms must be 0 (eg, x^2, x y)
+                    status &= (fabs(distort->y->coeff[i][j][0][0]) < FLT_EPSILON);
+                }
+            }
+        }
+    }
+    return status;
+}
Index: /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryUtils.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryUtils.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryUtils.h	(revision 15358)
@@ -0,0 +1,27 @@
+/* @file  pmAstrometryUtils.h
+ * @brief utility functions for transform and distort functions
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:14 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_UTILS_H
+#define PM_ASTROMETRY_UTILS_H
+
+/// @addtogroup Astrometry
+/// @{
+
+psPlane *psPlaneTransformGetCenter (psPlaneTransform *trans, double tol);
+psPlaneTransform *psPlaneTransformSetCenter (psPlaneTransform *output, psPlaneTransform *input, double Xo, double Yo);
+
+psPlaneTransform *psPlaneTransformIdentity (int order);
+psPlaneDistort *psPlaneDistortIdentity (int order);
+
+bool psPlaneTransformIsDiagonal (psPlaneTransform *transform);
+bool psPlaneDistortIsDiagonal (psPlaneDistort *distort);
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryWCS.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryWCS.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryWCS.c	(revision 15358)
@@ -0,0 +1,757 @@
+/** @file  pmAstrometryWCS.c
+ *
+ *  @brief functions to convert FITS WCS keywords to / from pmFPA structures
+ *
+ *  @ingroup Astrometry
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.23 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-10-09 19:27:04 $
+ *
+ *  Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <strings.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAExtent.h"
+#include "pmAstrometryWCS.h"
+#include "pmAstrometryUtils.h"
+#include "pmAstrometryRegions.h"
+
+// the following functions support coordinate transformations direcly related to the FITS WCS
+// keywords.  The FITS WCS allows for only a single level of transformation, thus it is not
+// appropriate for mosaic astrometry consisting of telescope distortion plus chip terms.
+// Below, we support the Elixir convention of using two connected FITS headers to define two
+// levels of coordinate transformation.  In the pmFPA structure, the projection, distortion,
+// and FPA-to-Chip transformations are carried independently.  NOTE: The FITS WCS keywords do
+// not represent a simple polynomial.  Instead, they have no constant term, and the coordinates
+// are corrected to a reference pixel before the polynomial transformation is applied.
+
+// interpret header WCS (only handles traditional WCS for the moment)
+// pixelScale is microns per pixel
+bool pmAstromReadWCS (pmFPA *fpa, pmChip *chip, const psMetadata *header, double pixelScale)
+{
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+    if (!wcs) {
+        return false;
+    }
+
+    bool status = pmAstromWCStoFPA (fpa, chip, wcs, pixelScale);
+
+    psFree (wcs);
+    return status;
+}
+
+// convert toFPA / toSky components to pmAstromWCS
+// tolerance is convergence for inversion of non-linear terms in pixels
+bool pmAstromWriteWCS (psMetadata *header, const pmFPA *fpa, const pmChip *chip, double tol)
+{
+    // XXX require chip->toFPA->x->nX == chip->toFPA->x->nY
+    // XXX require chip->toFPA->y->nX == chip->toFPA->y->nY
+    // XXX require chip->toFPA->x->nX == chip->toFPA->y->nX
+    // XXX require chip->toFPA->nX == 1,2,3
+
+    // technically, we can have a plate scale here (fpa->toTPA:dx,dy != 1)
+    if (!psPlaneTransformIsDiagonal (fpa->toTPA))
+        psAbort("invalid TPA transformation");
+
+    pmAstromWCS *wcs = pmAstromWCSfromFPA(fpa, chip, tol);
+    pmAstromWCStoHeader (header, wcs);
+
+    psFree (wcs);
+    return true;
+}
+
+// interpret chip header WCS as bilevel chip components
+bool pmAstromReadBilevelChip (pmChip *chip, const psMetadata *header)
+{
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+    if (!wcs) {
+        return false;
+    }
+
+    bool status = pmAstromWCSBileveltoChip (chip, wcs);
+
+    psFree (wcs);
+    return status;
+}
+
+// convert toFPA / toSky components to traditional WCS
+// we require the header to have NAXIS1,NAXIS2, the field of the FPA 
+// the center of the TPA/Sky projection is 0.5*(NAXIS1,NAXIS2)
+bool pmAstromReadBilevelMosaic (pmFPA *fpa, const psMetadata *header)
+{
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+    if (!wcs) {
+        psError(PS_ERR_UNKNOWN, false, "failure to determine WCS terms from header");
+        return false;
+    }
+
+    bool status1 = false;
+    bool status2 = false;
+    int Nx = psMetadataLookupS32 (&status1, header, "NAXIS1");
+    int Ny = psMetadataLookupS32 (&status2, header, "NAXIS2");
+    if (!status1 || !status2) {
+	psFree (wcs);
+        psError(PS_ERR_UNKNOWN, false, "missing required FPA size in header");
+        return false;
+    }
+
+    psRegion region = psRegionSet (-0.5*Nx, +0.5*Nx, -0.5*Ny, +0.5*Ny);
+    bool status = pmAstromWCSBileveltoFPA (fpa, wcs, region);
+
+    psFree (wcs);
+    return status;
+}
+
+// convert chip->toFPA components to bilevel WCS
+bool pmAstromWriteBilevelChip (psMetadata *header, const pmChip *chip, double tol)
+{
+    pmAstromWCS *wcs = pmAstromWCSBilevelChipFromFPA (chip, tol);
+
+    pmAstromWCStoHeader (header, wcs);
+
+    psFree (wcs);
+    return true;
+}
+
+
+// convert fpa->toTPA, fpa->toSky components to bilevel WCS
+bool pmAstromWriteBilevelMosaic (psMetadata *header, const pmFPA *fpa, double tol)
+{
+    pmAstromWCS *wcs = pmAstromWCSBilevelMosaicFromFPA (fpa, tol);
+
+    // we need to specify the dimensions of the FPA
+    // if we have chips defined, we can do
+    psRegion *region = pmAstromFPAExtent (fpa);
+    int Nx = region->x1 - region->x0;
+    int Ny = region->y1 - region->y0;
+    psMetadataAddS32 (header, PS_LIST_TAIL, "NAXIS1", PS_META_REPLACE, "Mosaic Dimensions", Nx);
+    psMetadataAddS32 (header, PS_LIST_TAIL, "NAXIS2", PS_META_REPLACE, "Mosaic Dimensions", Ny);
+
+    pmAstromWCStoHeader (header, wcs);
+
+    psFree (region);
+    psFree (wcs);
+    return true;
+}
+
+// convert coordinates from chip to sky using a pmAstromWCS structure
+bool pmAstromWCStoSky (psSphere *sky, pmAstromWCS *wcs, psPlane *chip)
+{
+
+    if (chip == NULL)
+        return false;
+    if (sky == NULL)
+        return false;
+    if (wcs == NULL)
+        return false;
+
+    psPlane *Chip = psPlaneAlloc();
+    psPlane *FP = psPlaneAlloc();
+
+    Chip->x = chip->x - wcs->crpix1;
+    Chip->y = chip->y - wcs->crpix2;
+
+    psPlaneTransformApply (FP, wcs->trans, Chip);
+    psDeproject (sky, FP, wcs->toSky); // find the RA,DEC coord of the focal-plane coordinate
+
+    psFree (Chip);
+    psFree (FP);
+    return true;
+}
+
+// convert coordinates from sky to chip using a pmAstromWCS structure
+bool pmAstromWCStoChip (psPlane *chip, pmAstromWCS *wcs, psSphere *sky)
+{
+
+    if (chip == NULL)
+        return false;
+    if (sky == NULL)
+        return false;
+    if (wcs == NULL)
+        return false;
+
+    psError(PS_ERR_UNKNOWN, true, "not yet implemented: needs to invert the transformation");
+    return false;
+
+    psPlane *Chip = psPlaneAlloc();
+    psPlane *FP = psPlaneAlloc();
+
+    psProject (FP, sky, wcs->toSky); // find the RA,DEC coord of the focal-plane coordinate
+
+    // I need the inverse of wcs->transform at this point
+    psPlaneTransformApply (Chip, wcs->trans, FP);
+
+    chip->x = Chip->x + wcs->crpix1;
+    chip->y = Chip->y + wcs->crpix2;
+
+    psFree (Chip);
+    psFree (FP);
+    return true;
+}
+
+// interpret header WCS keywords (valid for bilevel and traditional WCS)
+pmAstromWCS *pmAstromWCSfromHeader (const psMetadata *header)
+{
+    psProjectionType type;
+    bool status, pcKeys, cdKeys, isPoly;
+    char name[16]; // used to store FITS keyword below (always < 8, so 16 should be safe!)
+
+    // interpret header data, convert to crval(i), etc
+    char *ctype = psMetadataLookupPtr (&status, header, "CTYPE2");
+    if (!status) {
+        psLogMsg ("psastro", 2, "warning: no WCS metadata in header\n");
+        return NULL;
+    }
+
+    // determine projection type
+    // XXX there are two indications for higher-order terms: the type (DIS,WRP,PLY,ZPL) and
+    // the value of NPLYTERM.
+    type = psProjectTypeFromString (ctype);
+    if (type == PS_PROJ_NTYPE) {
+        psLogMsg ("psastro", 2, "warning: unknown projection type %s\n", ctype);
+        return NULL;
+    }
+
+    // what type of WCS keywords are available?
+    // XXX add check for CROTA2
+    int fitOrder = psMetadataLookupS32 (&isPoly, header, "NPLYTERM");
+    psMetadataLookupF64 (&pcKeys, header, "PC001001");
+    psMetadataLookupF64 (&cdKeys, header, "CD1_1");
+
+    if (cdKeys && pcKeys) {
+        // XXX make this an option
+        psLogMsg ("psastro", 2, "warning: both CDi_j and PC00i00j defined in headers, using PC00i00j terms\n");
+    }
+    if (!cdKeys && !pcKeys) {
+        psError(PS_ERR_UNKNOWN, true, "missing both CDi_j and PC00i00j WCS terms");
+        // XXX we could default here to RA, DEC, ROTANGLE
+        return NULL;
+    }
+    if (isPoly) {
+        if (!pcKeys) {
+            psError(PS_ERR_UNKNOWN, true, "polynomial terms defined, but missing PC00i00j WCS terms");
+            return NULL;
+        }
+        if (fitOrder == 0)
+            fitOrder = 1;
+        if ((fitOrder > 3) || (fitOrder < 1)) {
+            psError(PS_ERR_UNKNOWN, true, "NPLYTERM value undefined: %d", fitOrder);
+            return NULL;
+        }
+    } else {
+        fitOrder = 1;
+    }
+
+    pmAstromWCS *wcs = pmAstromWCSAlloc (fitOrder, fitOrder);
+
+    // construct a transformation from X,Y in pixels to L,M in pixels
+    // NOTE that the WCS keywords convert X,Y to degrees first (using cdelt1,2)
+    // and then define a transformation from degrees to degrees
+
+    wcs->crval1 = psMetadataLookupF64 (&status, header, "CRVAL1");
+    wcs->crval2 = psMetadataLookupF64 (&status, header, "CRVAL2");
+    wcs->crpix1 = psMetadataLookupF64 (&status, header, "CRPIX1");
+    wcs->crpix2 = psMetadataLookupF64 (&status, header, "CRPIX2");
+    wcs->toSky = psProjectionAlloc (wcs->crval1*PM_RAD_DEG, wcs->crval2*PM_RAD_DEG, PM_RAD_DEG, PM_RAD_DEG, type);
+    // XXX I think this is wrong for linear proj
+
+    // test the CDELTi varient
+    if (pcKeys) {
+        wcs->cdelt1 = psMetadataLookupF64 (&status, header, "CDELT1");
+        wcs->cdelt2 = psMetadataLookupF64 (&status, header, "CDELT2");
+
+        // test the CROTAi varient:
+        // XXX double check lambda..
+        double rotate = psMetadataLookupF64 (&status, header, "CROTA2");
+        if (status) {
+            wcs->trans->x->coeff[1][0] = +wcs->cdelt1 * cos(rotate*PM_RAD_DEG); // == PC1_1
+            wcs->trans->x->coeff[0][1] = -wcs->cdelt2 * sin(rotate*PM_RAD_DEG); // == PC1_2
+            wcs->trans->y->coeff[1][0] = +wcs->cdelt1 * sin(rotate*PM_RAD_DEG); // == PC2_1
+            wcs->trans->y->coeff[0][1] = +wcs->cdelt2 * cos(rotate*PM_RAD_DEG); // == PC2_2
+            return wcs;
+        }
+
+        // FITS WCS PCi,j has units of unity
+        // wcs->trans has units of degrees/pixel
+        wcs->trans->x->coeff[1][0] = wcs->cdelt1 * psMetadataLookupF64 (&status, header, "PC001001"); // == PC1_1
+        wcs->trans->x->coeff[0][1] = wcs->cdelt2 * psMetadataLookupF64 (&status, header, "PC001002"); // == PC1_2
+        wcs->trans->y->coeff[1][0] = wcs->cdelt1 * psMetadataLookupF64 (&status, header, "PC002001"); // == PC2_1
+        wcs->trans->y->coeff[0][1] = wcs->cdelt2 * psMetadataLookupF64 (&status, header, "PC002002"); // == PC2_2
+
+        if (isPoly) {
+            // Elixir-style polynomial terms
+            // XXX currently, Elixir/DVO cannot accept mixed orders
+            for (int i = 0; i <= fitOrder; i++) {
+                for (int j = 0; j <= fitOrder; j++) {
+                    if (i + j < 2)
+                        continue;
+                    if (i + j > fitOrder) {
+                        wcs->trans->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+                        wcs->trans->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+                        continue;
+                    }
+                    sprintf (name, "PCA1X%1dY%1d", i, j);
+                    wcs->trans->x->coeff[i][j] = pow(wcs->cdelt1, i) * pow(wcs->cdelt2, j) * psMetadataLookupF64 (&status, header, name);
+                    sprintf (name, "PCA2X%1dY%1d", i, j);
+                    wcs->trans->y->coeff[i][j] = pow(wcs->cdelt1, i) * pow(wcs->cdelt2, j) * psMetadataLookupF64 (&status, header, name);
+                }
+            }
+        }
+        return wcs;
+    }
+
+    // test the CDi_j varient
+    if (cdKeys) {
+        wcs->trans->x->coeff[1][0] = psMetadataLookupF64 (&status, header, "CD1_1"); // == PC1_1
+        wcs->trans->x->coeff[0][1] = psMetadataLookupF64 (&status, header, "CD1_2"); // == PC1_2
+        wcs->trans->y->coeff[1][0] = psMetadataLookupF64 (&status, header, "CD2_1"); // == PC2_1
+        wcs->trans->y->coeff[0][1] = psMetadataLookupF64 (&status, header, "CD2_2"); // == PC2_2
+        wcs->cdelt1 = hypot (wcs->trans->x->coeff[1][0], wcs->trans->x->coeff[0][1]);
+        wcs->cdelt2 = hypot (wcs->trans->y->coeff[1][0], wcs->trans->y->coeff[0][1]);
+        return wcs;
+    }
+    psLogMsg ("psastro", 2, "warning: missing rotation matrix?\n");
+    psFree (wcs);
+    return NULL;
+}
+
+// convert wcs transformations into header WCS keywords (only handles traditional WCS for the moment)
+// wcs->trans defines the transformation from pixels to degrees.
+// wcs->cdelt1,2 carries the original pixels scale.
+// XXX force PC00i00j to be normalized, or use cdelt1,2 to set the scale?
+// here I've chosen to force the rotation matrix to be normalized
+bool pmAstromWCStoHeader (psMetadata *header, const pmAstromWCS *wcs)
+{
+    char name[16]; // used to store FITS keyword below (always < 8, so 16 should be safe!)
+    char *type;
+
+    type = psProjectTypeToString (wcs->toSky->type, "RA--");
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE1", PS_META_REPLACE, "", type);
+    psFree (type);
+
+    type = psProjectTypeToString (wcs->toSky->type, "DEC-");
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE2", PS_META_REPLACE, "", type);
+    psFree (type);
+
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRVAL1", PS_META_REPLACE, "", wcs->toSky->R*PM_DEG_RAD);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRVAL2", PS_META_REPLACE, "", wcs->toSky->D*PM_DEG_RAD);
+
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRPIX1", PS_META_REPLACE, "", wcs->crpix1);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRPIX2", PS_META_REPLACE, "", wcs->crpix2);
+
+    // XXX make it optional to write out CDi_j terms, or other versions
+    // apply CDELT1,2 (degrees / pixel) to yield PCi,j terms of order unity
+    double cdelt1 = wcs->cdelt1;
+    double cdelt2 = wcs->cdelt2;
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CDELT1", PS_META_REPLACE, "", cdelt1);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CDELT2", PS_META_REPLACE, "", cdelt2);
+
+    // test the PC00i00j varient:
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC001001", PS_META_REPLACE, "", wcs->trans->x->coeff[1][0] / cdelt1); // == PC1_1
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC001002", PS_META_REPLACE, "", wcs->trans->x->coeff[0][1] / cdelt2); // == PC1_2
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC002001", PS_META_REPLACE, "", wcs->trans->y->coeff[1][0] / cdelt1); // == PC2_1
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC002002", PS_META_REPLACE, "", wcs->trans->y->coeff[0][1] / cdelt2); // == PC2_2
+
+    // Elixir-style polynomial terms
+    // XXX currently, Elixir/DVO cannot accept mixed orders
+    // XXX need to respect the masks
+    // XXX is wcs->cdelt1,2 always consistent?
+    int fitOrder = wcs->trans->x->nX;
+    if (fitOrder > 1) {
+        for (int i = 0; i <= fitOrder; i++) {
+            for (int j = 0; j <= fitOrder; j++) {
+                if (i + j < 2)
+                    continue;
+                if (i + j > fitOrder)
+                    continue;
+                sprintf (name, "PCA1X%1dY%1d", i, j);
+                psMetadataAddF64 (header, PS_LIST_TAIL, name, PS_META_REPLACE, "", wcs->trans->x->coeff[i][j] / pow(cdelt1, i) / pow(cdelt2, j));
+                sprintf (name, "PCA2X%1dY%1d", i, j);
+                psMetadataAddF64 (header, PS_LIST_TAIL, name, PS_META_REPLACE, "", wcs->trans->y->coeff[i][j] / pow(cdelt1, i) / pow(cdelt2, j));
+            }
+        }
+        psMetadataAddS32 (header, PS_LIST_TAIL, "NPLYTERM", PS_META_REPLACE, "", fitOrder);
+    }
+
+    return (true);
+}
+
+// interpret header WCS (only handles traditional WCS for the moment)
+// pixelScale is the pixel size in microns/pixel
+bool pmAstromWCStoFPA (pmFPA *fpa, pmChip *chip, const pmAstromWCS *wcs, double pixelScale)
+{
+    psPlaneTransform *toFPA;
+
+    // create transformation with 0,0 reference pixel and units of degrees/pixel
+    toFPA = psPlaneTransformSetCenter (NULL, wcs->trans, -wcs->crpix1, -wcs->crpix2);
+
+    // modify scale of toFPA to have units of microns/pixel
+    // cdelt1,2 has units of degree/pixel
+    for (int i = 0; i <= toFPA->x->nX; i++) {
+        for (int j = 0; j <= toFPA->x->nX; j++) {
+            toFPA->x->coeff[i][j] *= pixelScale/wcs->cdelt1;
+            toFPA->y->coeff[i][j] *= pixelScale/wcs->cdelt2;
+        }
+    }
+
+    // pdelt1,2 has units of degree/micron
+    double pdelt1 = wcs->cdelt1 / pixelScale;
+    double pdelt2 = wcs->cdelt2 / pixelScale;
+
+    // projection from TPA (linear microns) to SKY (radians)
+    psProjection *toSky = psProjectionAlloc (wcs->toSky->R, wcs->toSky->D, PM_RAD_DEG*pdelt1, PM_RAD_DEG*pdelt2, wcs->toSky->type);
+
+    if (fpa->toSky == NULL) {
+        fpa->toTPA = psPlaneTransformIdentity (1);
+        fpa->fromTPA = psPlaneTransformIdentity (1);
+        fpa->toSky = toSky;
+    } else {
+
+        // this section allows the loaded chip to be included in an fpa structure in which
+        // other chips have already been loaded (ie, the fpa->toTPA, fpa->toSky components have
+        // already been defined).  we have to adjust to match the existing transformation.
+
+        if (fpa->toTPA == NULL)
+            psAbort("projection defined, tangent-plane not defined");
+        if (fpa->fromTPA == NULL)
+            psAbort("projection defined, tangent-plane not defined");
+
+        // convert from pixels on this chip to pixels on reference chip
+        // rX has units of refpixels / pixel
+        double rX = toSky->Xs / fpa->toSky->Xs;
+        double rY = toSky->Ys / fpa->toSky->Ys;
+
+        for (int i = 0; i <= fpa->toTPA->x->nX; i++) {
+            for (int j = 0; j <= fpa->toTPA->x->nY; j++) {
+                toFPA->x->coeff[i][j] *= rX;
+                toFPA->y->coeff[i][j] *= rY;
+            }
+        }
+
+        // adjust reference pixel for new toSky reference coordinate
+        // find the FPA coordinate of 0,0 for this chip.
+        psPlane *fpOld = psPlaneAlloc();
+        psPlane *fpNew = psPlaneAlloc();
+        psPlane *tp = psPlaneAlloc();
+        psSphere *sky = psSphereAlloc();
+
+	sky->r = toSky->R;
+	sky->d = toSky->D;
+        psProject (tp, sky, fpa->toSky); // find the focal-plane coord of this RA,DEC coord using the ref chip projection
+        psPlaneTransformApply (fpOld, fpa->fromTPA, tp);
+
+	sky->r = fpa->toSky->R;
+	sky->d = fpa->toSky->D;
+        psProject (tp, sky, fpa->toSky); // find the focal-plane coord of this RA,DEC coord using the ref chip projection
+        psPlaneTransformApply (fpNew, fpa->fromTPA, tp);
+
+        toFPA->x->coeff[0][0] -= fpNew->x - fpOld->x;
+        toFPA->y->coeff[0][0] -= fpNew->y - fpOld->y;
+
+        psFree (sky);
+        psFree (tp);
+        psFree (fpOld);
+        psFree (fpNew);
+
+        psFree (toSky);
+    }
+
+    // free an existing toFPA structure
+    psFree (chip->toFPA);
+    chip->toFPA = toFPA;
+
+    // determine the inverse transformation: we need the chip pixels covered by this transform
+    psRegion *region = pmChipPixels (chip);
+
+    psFree (chip->fromFPA);
+    chip->fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50);
+    psFree (region);
+
+    // this can take a very long time...
+    while (fpa->toSky->R < 0)
+        fpa->toSky->R += 2.0*M_PI;
+    while (fpa->toSky->R > 2.0*M_PI)
+        fpa->toSky->R -= 2.0*M_PI;
+
+    psTrace ("psastro", 5, "toFPA: %f %f  (%f,%f),(%f,%f)\n",
+             chip->toFPA->x->coeff[0][0], chip->toFPA->y->coeff[0][0],
+             chip->toFPA->x->coeff[1][0], chip->toFPA->x->coeff[0][1],
+             chip->toFPA->y->coeff[1][0], chip->toFPA->y->coeff[0][1]);
+
+    psTrace ("psastro", 5, "frFPA: %f %f  (%f,%f),(%f,%f)\n",
+             chip->fromFPA->x->coeff[0][0], chip->fromFPA->y->coeff[0][0],
+             chip->fromFPA->x->coeff[1][0], chip->fromFPA->x->coeff[0][1],
+             chip->fromFPA->y->coeff[1][0], chip->fromFPA->y->coeff[0][1]);
+
+    return true;
+}
+
+// convert a pmAstromWCS structure representing a bilevel chip into corresponding chip elements
+bool pmAstromWCSBileveltoChip (pmChip *chip, const pmAstromWCS *wcs)
+{
+    /* we convert wcs->trans to toFPA, which is different from wcs->trans in 3 important ways:
+     * 1) the output is in pixel (not degrees): divide by cdelt1,2 raised to an appropriate power
+     * 2) X,Y are applied directly, without an applied Xo,Yo offset
+     * 3) there is an allowed Lo,Mo term ([0][0] coefficients)
+     */
+
+    // create transformation with 0,0 reference pixel and units of microns/pixel
+    psFree (chip->toFPA);
+    chip->toFPA = psPlaneTransformSetCenter (NULL, wcs->trans, -wcs->crpix1, -wcs->crpix2);
+
+    // determine the inverse transformation: we need the chip pixels covered by this transform
+    psRegion *region = pmChipPixels (chip);
+
+    psFree (chip->fromFPA);
+    chip->fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50);
+    psFree (region);
+
+    return true;
+}
+
+// convert a pmAstromWCS structure representing a bilevel mosaic into corresponding fpa elements
+bool pmAstromWCSBileveltoFPA (pmFPA *fpa, const pmAstromWCS *wcs, psRegion region)
+{
+    // projection from TPA (microns) to SKY (radians)
+    // cdelt1,2 has units of degrees/micron
+    fpa->toSky = psProjectionAlloc (wcs->toSky->R, wcs->toSky->D, wcs->cdelt1*PM_RAD_DEG, wcs->cdelt2*PM_RAD_DEG, wcs->toSky->type);
+
+    // create transformation with 0,0 reference pixel
+    fpa->toTPA = psPlaneTransformSetCenter (NULL, wcs->trans, -wcs->crpix1, -wcs->crpix2);
+
+    // convert fpa->toTPA to units of unity (microns/micron)
+    for (int i = 0; i <= fpa->toTPA->x->nX; i++) {
+        for (int j = 0; j <= fpa->toTPA->x->nY; j++) {
+            fpa->toTPA->x->coeff[i][j] /= wcs->cdelt1;
+            fpa->toTPA->y->coeff[i][j] /= wcs->cdelt2;
+        }
+    }
+
+    // the transformation used the region to define the inversion grid
+    // the region defines the FPA pixels covered by the tranformation
+    psFree (fpa->fromTPA);
+    fpa->fromTPA = psPlaneTransformInvert(NULL, fpa->toTPA, region, 50);
+    return true;
+}
+
+// convert toFPA / toSky components to pmAstromWCS
+// tolerance is allowed error in center solution in pixels
+pmAstromWCS *pmAstromWCSfromFPA (const pmFPA *fpa, const pmChip *chip, double tol)
+{
+
+    // technically, we can have a plate scale here (fpa->toTPA:dx,dy != 1)
+    // XXX not really: toTPA needs to have unity scale for distortion fitting function
+    if (!psPlaneTransformIsDiagonal (fpa->toTPA))
+        psAbort("invalid TPA transformation");
+
+    // XXX require chip->toFPA->x->nX == chip->toFPA->x->nY
+    // XXX require chip->toFPA->y->nX == chip->toFPA->y->nY
+    // XXX require chip->toFPA->x->nX == chip->toFPA->y->nX
+    // XXX require chip->toFPA->nX == 1,2,3
+
+    pmAstromWCS *wcs = pmAstromWCSAlloc(chip->toFPA->x->nX, chip->toFPA->x->nY);
+
+    // convert projection from FPA to SKY into wcs projection (degrees to radians)
+    wcs->toSky = psProjectionAlloc (fpa->toSky->R, fpa->toSky->D, PM_RAD_DEG, PM_RAD_DEG, fpa->toSky->type);
+    wcs->crval1 = fpa->toSky->R*PM_DEG_RAD;
+    wcs->crval2 = fpa->toSky->D*PM_DEG_RAD;
+
+    // given transformation, solve for coordinates which yields output coordinates of 0,0
+    psPlane *center = psPlaneTransformGetCenter (chip->toFPA, tol);
+
+    // create wcs transform from toFPA, resulting transformation has units of microns/pixel
+    // adjust wcs transform to use center as reference coordinate
+    psPlaneTransformSetCenter (wcs->trans, chip->toFPA, center->x, center->y);
+
+    // calculated center is crpix1,2
+    wcs->crpix1 = center->x;
+    wcs->crpix2 = center->y;
+    psFree (center);
+
+    // pdelt1,2 has units of degrees/micron
+    double pdelt1 = fpa->toSky->Xs * PM_DEG_RAD;
+    double pdelt2 = fpa->toSky->Ys * PM_DEG_RAD;
+
+    // convert wcs->trans to a matrix with units of degrees/pixel
+    for (int i = 0; i <= wcs->trans->x->nX; i++) {
+        for (int j = 0; j <= wcs->trans->x->nY; j++) {
+            wcs->trans->x->coeff[i][j] *= pdelt1;
+            wcs->trans->y->coeff[i][j] *= pdelt2;
+        }
+    }
+
+    // cdelt1,2 has units of degrees/pixel
+    wcs->cdelt1 = hypot (wcs->trans->x->coeff[1][0], wcs->trans->x->coeff[0][1]);
+    wcs->cdelt2 = hypot (wcs->trans->y->coeff[1][0], wcs->trans->y->coeff[0][1]);
+
+    return wcs;
+}
+
+/* the bilevel astrometry description consists of a polynomial warping from
+   chip coordinates to FPA coordinates (coords->ctype = LIN---WRP), followed
+   by a polynomial representation of the telescope distortion + the projection
+   (coords->ctype = RA---DIS).
+*/
+
+// convert the chip-level toFPA to a wcs polynomial transformation
+pmAstromWCS *pmAstromWCSBilevelChipFromFPA (const pmChip *chip, double tol)
+{
+    // XXX require chip->toFPA->x->nX == chip->toFPA->x->nY
+    // XXX require chip->toFPA->y->nX == chip->toFPA->y->nY
+    // XXX require chip->toFPA->x->nX == chip->toFPA->y->nX
+    // XXX require chip->toFPA->nX == 1,2,3
+
+    // convert chip->toFPA to wcs format (WRP)
+    pmAstromWCS *wcs = pmAstromWCSAlloc(chip->toFPA->x->nX, chip->toFPA->x->nY);
+
+    // Chip to FPA transformation is a Cartesian 'projection'
+    // reference pixel for FPA is 0.0, 0.0
+    wcs->toSky = psProjectionAlloc (0.0, 0.0, 1.0, 1.0, PS_PROJ_WRP);
+    wcs->crval1 = 0.0;
+    wcs->crval2 = 0.0;
+
+    // given transformation, solve for coordinates which yields output coordinates of 0,0
+    psPlane *center = psPlaneTransformGetCenter (chip->toFPA, tol);
+
+    // adjust wcs transform to use center as reference coordinate
+    // resulting transformation has units of microns/pixel
+    psPlaneTransformSetCenter (wcs->trans, chip->toFPA, center->x, center->y);
+
+    // calculated center is crpix1,2
+    wcs->crpix1 = center->x;
+    wcs->crpix2 = center->y;
+    psFree (center);
+
+    // output coordinates are in microns : CDELT1,2 has units of microns/pixel
+    wcs->cdelt1 = hypot (wcs->trans->x->coeff[1][0], wcs->trans->x->coeff[0][1]);
+    wcs->cdelt2 = hypot (wcs->trans->y->coeff[1][0], wcs->trans->y->coeff[0][1]);
+
+    return wcs;
+}
+
+// convert the fpa-level toTPA, toSky to a wcs polynomial transformation
+pmAstromWCS *pmAstromWCSBilevelMosaicFromFPA (const pmFPA *fpa, double tol)
+{
+    // XXX require fpa->toTPA->x->nX == fpa->toTPA->x->nY
+    // XXX require fpa->toTPA->y->nX == fpa->toTPA->y->nY
+    // XXX require fpa->toTPA->x->nX == fpa->toTPA->y->nX
+    // XXX require fpa->toTPA->nX == 1,2,3
+    // XXX require fpa->toSky->type == PS_PROJ_TAN
+
+    // convert fpa->toTPA + fpa->toSky to wcs format (DIS)
+    pmAstromWCS *wcs = pmAstromWCSAlloc(fpa->toTPA->x->nX, fpa->toTPA->x->nY);
+
+    // convert projection from TPA to SKY into wcs projection (degrees to radians)
+    wcs->toSky = psProjectionAlloc (fpa->toSky->R, fpa->toSky->D, PM_RAD_DEG, PM_RAD_DEG, PS_PROJ_DIS);
+    wcs->crval1 = fpa->toSky->R*PM_DEG_RAD;
+    wcs->crval2 = fpa->toSky->D*PM_DEG_RAD;
+
+    // given transformation, solve for coordinates which yields output coordinates of 0,0
+    psPlane *center = psPlaneTransformGetCenter (fpa->toTPA, tol);
+
+    // adjust wcs transform to use center as reference coordinate
+    // resulting transformation has units of unity (microns/micron)
+    psPlaneTransformSetCenter (wcs->trans, fpa->toTPA, center->x, center->y);
+
+    // calculated center is crpix1,2
+    wcs->crpix1 = center->x;
+    wcs->crpix2 = center->y;
+    psFree (center);
+
+    // pdelt1,2 has units of degrees/micron
+    double pdelt1 = fpa->toSky->Xs * PM_DEG_RAD;
+    double pdelt2 = fpa->toSky->Ys * PM_DEG_RAD;
+
+    // convert wcs->trans to units of degree/micron
+    for (int i = 0; i <= wcs->trans->x->nX; i++) {
+        for (int j = 0; j <= wcs->trans->x->nY; j++) {
+            wcs->trans->x->coeff[i][j] *= pdelt1;
+            wcs->trans->y->coeff[i][j] *= pdelt2;
+        }
+    }
+
+    // cdelt1,2 has units of degrees/micron
+    wcs->cdelt1 = hypot (wcs->trans->x->coeff[1][0], wcs->trans->x->coeff[0][1]);
+    wcs->cdelt2 = hypot (wcs->trans->y->coeff[1][0], wcs->trans->y->coeff[0][1]);
+
+    return wcs;
+}
+
+static void pmAstromWCSFree (pmAstromWCS *wcs)
+{
+
+    if (!wcs)
+        return;
+    psFree (wcs->trans);
+    psFree (wcs->toSky);
+}
+
+pmAstromWCS *pmAstromWCSAlloc (int nXorder, int nYorder)
+{
+
+    pmAstromWCS *wcs = (pmAstromWCS *) psAlloc(sizeof(pmAstromWCS));
+    psMemSetDeallocator(wcs, (psFreeFunc) pmAstromWCSFree);
+
+    wcs->trans = psPlaneTransformAlloc (nXorder, nYorder);
+    wcs->toSky = NULL;
+
+    memset (wcs->ctype1, 0, PM_ASTROM_WCS_TYPE_SIZE);
+    memset (wcs->ctype2, 0, PM_ASTROM_WCS_TYPE_SIZE);
+    return wcs;
+}
+
+/*****
+ 
+For mosaic astrometry, we need to have a starting set of projection terms in which the
+chip-to-FPA terms result in a fixed physical unit on the focal plane (eg, pixels or
+microns).  This set of projections, coupled with an identity toTPA (ie, no distortion) will
+result in substantial errors between the observed and predicted star positions on the focal
+plane: this is the measurement of the optical distortion in the camera.  At the same time,
+we need to carry around the transformations which allow us to make an accurate calculation
+of the position of the stars based on the input (per-chip) astrometry.  These
+transformations will allow us to match the raw and ref stars robustly.  To convert the
+per-chip astrometry (which may have been calculated with a different plate scale for each
+chip) to a collection of astrometry terms for chips in a single mosaic, we need to adjust
+the chip-to-FPA scaling (eg, pc11) to match the variations in the effective plate scale for
+each chip (eg, cdelt1).  Thus, we need to carry around both the
+ 
+*****/
+
+/* discussion of the coord transformations:
+   X,Y: coord on a chip in pixels
+   L,M: coord on the focal plane (pixels)
+   P,Q: coord in the tangent plane (microns or mm?)
+   R,D: coord on the sky 
+ 
+   this function creates WCS terms which convert directly from chip to sky.
+   this function requires a linear, unrotated toTPA distortion term
+   toTPA->x,y->coeff[1][0],[0][1] defines the detector scale (microns / pixel)
+   tpSky->Xs,Ys defines the plate scale (radians / micron)
+*/
+
+/* at this point, we have extracted from the header the WCS terms in the form of a polynomial,
+ * wcs->trans, which will convert X,Y in pixels to L,M in degrees.  we also have the following 
+ * elements defined:
+ * type (projection type)
+ * crval1,2 (in RA,DEC degrees)
+ * crpix1,2 
+ * cdelt1,2 (in degrees / pixel)
+ * pixelScale (microns / pixel)
+ * 
+ * now we convert wcs->trans to toFPA, which is different from wcs->trans in 3 important ways:
+ * 1) the output is in microns (not degrees): divide by cdelt1,2
+ * 2) X,Y are applied directly, without an applied Xo,Yo offset
+ * 3) there is an allowed Lo,Mo term ([0][0] coefficients)
+ */
+
Index: /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryWCS.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryWCS.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/astrom/pmAstrometryWCS.h	(revision 15358)
@@ -0,0 +1,77 @@
+/* @file  pmAstrometryDistortion.h
+ * @brief functions to convert FITS WCS keywords to / from pmFPA structures
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.11 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-21 22:00:49 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_WCS_H
+#define PM_ASTROMETRY_WCS_H
+
+/// @addtogroup Astrometry
+/// @{
+
+#define PM_ASTROM_WCS_TYPE_SIZE 80
+typedef struct
+{
+    char ctype1[PM_ASTROM_WCS_TYPE_SIZE];
+    char ctype2[PM_ASTROM_WCS_TYPE_SIZE];
+    double crval1, crval2;
+    double crpix1, crpix2;
+    double cdelt1, cdelt2;
+    psProjection *toSky;
+    psPlaneTransform *trans;
+}
+pmAstromWCS;
+
+// support function for the pmAstromWCS representation
+pmAstromWCS *pmAstromWCSAlloc (int nXorder, int nYorder);
+bool pmAstromWCStoSky (psSphere *sky, pmAstromWCS *wcs, psPlane *chip);
+bool pmAstromWCStoChip (psPlane *chip, pmAstromWCS *wcs, psSphere *sky);
+
+// read and write the pmAstromWCS representation to the header
+bool pmAstromWCStoHeader (psMetadata *header, const pmAstromWCS *wcs);
+pmAstromWCS *pmAstromWCSfromHeader (const psMetadata *header);
+
+// convert from wcs terms to chip->toFPA, fpa->toSky,toTPA terms
+bool pmAstromWCSBileveltoChip (pmChip *chip, const pmAstromWCS *wcs);
+bool pmAstromWCSBileveltoFPA (pmFPA *fpa, const pmAstromWCS *wcs, psRegion region);
+
+// convert from chip->toFPA, fpa->toSky,toTPA terms to wcs terms
+pmAstromWCS *pmAstromWCSBilevelChipFromFPA (const pmChip *chip, double tol);
+pmAstromWCS *pmAstromWCSBilevelMosaicFromFPA (const pmFPA *fpa, double tol);
+
+// convert the pmAstromWCS representation to the FPA representation
+bool pmAstromWCStoFPA (pmFPA *fpa, pmChip *chip, const pmAstromWCS *wcs, double plateScale);
+pmAstromWCS *pmAstromWCSfromFPA (const pmFPA *fpa, const pmChip *chip, double tol);
+
+// read wcs terms from the supplied header into the fpa hierarchy components
+bool pmAstromReadWCS (pmFPA *fpa, pmChip *chip, const psMetadata *header, double plateScale);
+
+// write the wcs terms from the fpa hierarchy components into the supplied header
+// tol is the convergence tolerance for the non-linear solution to the reference pixel
+bool pmAstromWriteWCS (psMetadata *header, const pmFPA *fpa, const pmChip *chip, double tol);
+
+bool pmAstromReadBilevelChip (pmChip *chip, const psMetadata *header);
+bool pmAstromReadBilevelMosaic (pmFPA *fpa, const psMetadata *header);
+
+bool pmAstromWriteBilevelChip (psMetadata *header, const pmChip *chip, double tol);
+bool pmAstromWriteBilevelMosaic (psMetadata *header, const pmFPA *fpa, double tol);
+
+// move to pslib
+psPlaneDistort *psPlaneDistortIdentity (int order);
+bool psPlaneDistortIsDiagonal (psPlaneDistort *distort);
+
+# define PM_DEG_RAD 57.295779513082322
+# define PM_RAD_DEG  0.017453292519943
+
+/// @}
+#endif // PM_ASTROMETRY_WCS_H
+
+/*
+ * the wcs->trans component defines a polynomial which converts (x-crpix1),(y-crpix2) to
+ * L,M in degrees
+ */
Index: /branches/eam_branch_20071023/psModules/src/camera/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/.cvsignore	(revision 15358)
@@ -0,0 +1,6 @@
+.deps
+.libs
+Makefile
+Makefile.in
+*.la
+*.lo
Index: /branches/eam_branch_20071023/psModules/src/camera/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/Makefile.am	(revision 15358)
@@ -0,0 +1,57 @@
+noinst_LTLIBRARIES = libpsmodulescamera.la
+
+libpsmodulescamera_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS)
+libpsmodulescamera_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulescamera_la_SOURCES  = \
+	pmFPA.c \
+	pmFPACalibration.c \
+	pmFPAConstruct.c \
+	pmFPACopy.c \
+	pmFPAHeader.c \
+	pmFPAMaskWeight.c \
+	pmFPAMosaic.c \
+	pmFPARead.c \
+	pmFPAUtils.c \
+	pmFPAWrite.c \
+	pmHDU.c \
+	pmHDUUtils.c \
+	pmHDUGenerate.c \
+	pmFPA_JPEG.c \
+	pmFPAview.c \
+	pmFPAfile.c \
+	pmFPAfileDefine.c \
+	pmFPAfileIO.c \
+	pmFPAfileFitsIO.c \
+	pmFPAFlags.c \
+	pmFPALevel.c \
+	pmFPAExtent.c \
+	pmCellSquish.c \
+	pmReadoutStack.c
+
+pkginclude_HEADERS = \
+	pmFPA.h \
+	pmFPACalibration.h \
+	pmFPAConstruct.h \
+	pmFPACopy.h \
+	pmFPAHeader.h \
+	pmFPAMaskWeight.h \
+	pmFPAMosaic.h \
+	pmFPARead.h \
+	pmFPAUtils.h \
+	pmFPAWrite.h \
+	pmHDU.h \
+	pmHDUUtils.h \
+	pmHDUGenerate.h \
+	pmFPA_JPEG.h \
+	pmFPAview.h \
+	pmFPAfile.h \
+	pmFPAfileDefine.h \
+	pmFPAfileIO.h \
+	pmFPAfileFitsIO.h \
+	pmFPAFlags.h \
+	pmFPALevel.h \
+	pmFPAExtent.h \
+	pmCellSquish.h \
+	pmReadoutStack.h
+
+CLEANFILES = *~
Index: /branches/eam_branch_20071023/psModules/src/camera/pmCellSquish.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmCellSquish.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmCellSquish.c	(revision 15358)
@@ -0,0 +1,176 @@
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmShifts.h"
+#include "pmCellSquish.h"
+
+// Comparing values to get ranges
+#define COMPARE_SMALLER(TARGET, SOURCE) if ((SOURCE) < (TARGET)) (TARGET) = (SOURCE);
+#define COMPARE_BIGGER(TARGET, SOURCE) if ((SOURCE) > (TARGET)) (TARGET) = (SOURCE);
+
+
+bool pmCellSquish(pmCell *cell, psMaskType maskVal, bool useShifts)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_ARRAY_NON_NULL(cell->readouts, false);
+    psArray *readouts = cell->readouts; // Array of readouts
+    long numReadouts = readouts->n; // Number of readouts
+
+    if (numReadouts <= 1) {
+        // We squished everything there was to squish
+        return true;
+    }
+
+    pmShifts *shifts = NULL;                   // Orthogonal transfer shifts
+    if (useShifts) {
+        bool mdok;                      // Status of MD lookup
+        shifts = psMetadataLookupPtr(&mdok, cell->analysis, PM_SHIFTS_TABLE_NAME);
+        if (!mdok || !shifts) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Squishing with shifts requested, but no shifts found.");
+            return false;
+        }
+        if (shifts->num != numReadouts) {
+            psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                    "Number of shifts (%ld) does not match number of readouts (%ld).",
+                    shifts->num, numReadouts);
+            return false;
+        }
+    }
+
+    // First pass to get the bounds, make sure everything is legit.
+    int xMin = 0, xMax = 0, yMin = 0, yMax = 0; // Bounds of the squish
+    bool valid = false;                 // Do we have a valid readout?
+    int col0 = 0, row0 = 0, numCols = 0, numRows = 0;// Window parameters for the readouts
+    int xShift = 0, yShift = 0;         // Shift due to orthogonal transfer, to be applied
+    if (useShifts && shifts->xyRelative) {
+        // Correct for final shift, to put in correct frame for the image that is read out
+        xShift = - shifts->x->data.S32[shifts->num - 1];
+        yShift = - shifts->y->data.S32[shifts->num - 1];
+    }
+    for (long i = 0; i < numReadouts; i++) {
+        // Add in the shift
+        if (useShifts) {
+            if (shifts->xyRelative) {
+                // Need to accumulate shift
+                xShift += shifts->x->data.S32[i];
+                yShift += shifts->y->data.S32[i];
+            } else {
+                // Correct for final shift, to put in correct frame for the image that is read out
+                xShift = shifts->x->data.S32[i] - shifts->x->data.S32[shifts->num - 1];
+                yShift = shifts->y->data.S32[i] - shifts->y->data.S32[shifts->num - 1];
+            }
+        }
+
+        pmReadout *readout = readouts->data[i]; // Readout of interest
+        if (!readout || !readout->image) {
+            continue;
+        }
+
+        if (!valid) {
+            valid = true;
+
+            col0 = readout->col0;
+            row0 = readout->row0;
+            numCols = readout->image->numCols;
+            numRows = readout->image->numRows;
+
+            if (useShifts) {
+                xMin = col0;
+                xMax = col0 + numCols;
+                yMin = row0;
+                yMax = row0 + numRows;
+            }
+        } else {
+            if (readout->col0 != col0 || readout->row0 != row0 ||
+                readout->image->numCols != numCols || readout->image->numRows != numRows) {
+                // Everything should have the same window because we've read it in from an image cube
+                psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                        "Readout window [%d:%d,%d:%d] doesn't match canonical window [%d:%d,%d:%d]",
+                        readout->col0, readout->col0 + readout->image->numCols,
+                        readout->row0, readout->row0 + readout->image->numRows,
+                        col0, col0 + numCols, row0, row0 + numRows);
+                return false;
+            }
+
+            if (useShifts) {
+                // If there is shifting, the actual window may change
+                int xMinTest = readout->col0 + xShift; // Minimum x value
+                int xMaxTest = readout->col0 + readout->image->numCols + xShift; // Maximum x value
+                int yMinTest = readout->row0 + yShift; // Minimum y value
+                int yMaxTest = readout->row0 + readout->image->numRows + yShift; // Maximum y value
+                COMPARE_SMALLER(xMin, xMinTest);
+                COMPARE_BIGGER(xMax, xMaxTest);
+                COMPARE_SMALLER(yMin, yMinTest);
+                COMPARE_BIGGER(yMax, yMaxTest);
+            }
+        }
+    }
+
+    if (useShifts) {
+        // Size of combined image, after shifts applied
+        numCols = xMax - xMin + 1;
+        numRows = yMax - yMin + 1;
+    }
+    psImage *squishImage = psImageAlloc(numCols, numRows, PS_TYPE_F32); // Squished image
+    psImageInit(squishImage, 0.0);
+    psImage *squishMask = psImageAlloc(numCols, numRows, PS_TYPE_MASK); // Squished mask
+    psImageInit(squishMask, 0);
+
+    // Second pass to do the squish
+    xShift = yShift = 0;                // Reset the accumulated shifts
+    if (useShifts && shifts->xyRelative) {
+        // Correct for final shift, to put in correct frame for the image that is read out
+        xShift = - shifts->x->data.S32[shifts->num - 1];
+        yShift = - shifts->y->data.S32[shifts->num - 1];
+    }
+    for (long i = 0; i < numReadouts; i++) {
+        if (useShifts) {
+            if (shifts->xyRelative) {
+                // Need to accumulate shift
+                xShift += shifts->x->data.S32[i];
+                yShift += shifts->y->data.S32[i];
+            } else {
+                // Correct for final shift, to put in correct frame for the image that is read out
+                xShift = shifts->x->data.S32[i] - shifts->x->data.S32[shifts->num - 1];
+                yShift = shifts->y->data.S32[i] - shifts->y->data.S32[shifts->num - 1];
+            }
+        }
+
+        pmReadout *readout = readouts->data[i]; // Readout of interest
+        if (!readout || !readout->image) {
+            continue;
+        }
+
+        int xOffset = xMin - readout->col0; // Offset to squished readout in x
+        int yOffset = yMin - readout->row0; // Offset to squished readout in y
+        if (useShifts) {
+            xOffset += xShift;
+            yOffset += yShift;
+        }
+
+        psImage *image = readout->image; // The image of interest
+        psImage *mask = readout->mask; // The mask of interest
+        for (int y = 0; y < readout->image->numRows; y++) {
+            int ySquish = y + yOffset; // Position on squished readout in y
+            for (int x = 0; x < readout->image->numCols; x++) {
+                int xSquish = x + xOffset; // Position on squished readout in x
+                squishImage->data.F32[ySquish][xSquish] += image->data.F32[y][x];
+                if (mask) {
+                    squishMask->data.PS_TYPE_MASK_DATA[ySquish][xSquish] |= mask->data.U8[y][x] & maskVal;
+                }
+            }
+        }
+    }
+
+    pmCellFreeReadouts(cell);
+    pmReadout *squishRO = pmReadoutAlloc(cell); // New readout to hold squished image
+    squishRO->image = squishImage;
+    squishRO->mask = squishMask;
+    squishRO->row0 = yMin;
+    squishRO->col0 = xMin;
+    psFree(squishRO);               // Drop reference
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmCellSquish.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmCellSquish.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmCellSquish.h	(revision 15358)
@@ -0,0 +1,13 @@
+#ifndef PM_CELL_SQUISH_H
+#define PM_CELL_SQUISH_H
+
+/// Squish (combine all component readouts of) a cell
+///
+/// The component readouts are combined, optionally taking into account orthogonal transfer shifts (assumed to
+/// already have been read) and masks.
+bool pmCellSquish(pmCell *cell,         ///< Cell to have readouts combined
+                  psMaskType maskVal,   ///< Value to be masked
+                  bool useShifts        ///< Use the shifts when squishing?
+    );
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPA.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPA.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPA.c	(revision 15358)
@@ -0,0 +1,420 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmConcepts.h"
+#include "pmMaskBadPixels.h"
+
+static void readoutFree(pmReadout *readout)
+{
+    // if this readout has a parent, drop that instance
+    if (readout->parent) {
+        psTrace("psModules.camera", 9, "Removing readout %zd from cell %zd...\n",
+                (size_t)readout, (size_t)readout->parent);
+        psArray *readouts = readout->parent->readouts;
+        for (int i = 0; i < readouts->n; i++) {
+            if (readouts->data[i] == readout) {
+                readouts->data[i] = NULL;
+            }
+        }
+    }
+    psTrace("psModules.camera", 9, "Freeing readout %zd\n", (size_t) readout);
+
+    psFree(readout->image);
+    psFree(readout->mask);
+    psFree(readout->weight);
+    psFree(readout->analysis);
+    psFree(readout->bias);
+}
+
+static void cellFree(pmCell *cell)
+{
+
+    // if this cell has a parent, drop that instance
+    if (cell->parent) {
+        psTrace("psModules.camera", 9, "Removing cell %zd from chip %zd...\n",
+                (size_t)cell, (size_t)cell->parent);
+        psArray *cells = cell->parent->cells;
+        for (int i = 0; i < cells->n; i++) {
+            if (cells->data[i] == cell) {
+                cells->data[i] = NULL;
+            }
+        }
+    }
+    psTrace("psModules.camera", 9, "Freeing cell %zd\n", (size_t)cell);
+    pmCellFreeReadouts(cell);
+    psFree(cell->readouts);
+
+    psFree(cell->concepts);
+    psFree(cell->analysis);
+    psFree(cell->config);
+    psFree(cell->hdu);
+}
+
+static void chipFree(pmChip* chip)
+{
+    // if this chip has a parent, drop that instance
+    if (chip->parent) {
+        psTrace("psModules.camera", 9, "Removing chip %zd from fpa %zd...\n",
+                (size_t)chip, (size_t)chip->parent);
+        psArray *chips = chip->parent->chips;
+        for (int i = 0; i < chips->n; i++) {
+            if (chips->data[i] == chip) {
+                chips->data[i] = NULL;
+            }
+        }
+    }
+
+    psTrace("psModules.camera", 9, "Freeing chip %zd\n", (size_t)chip);
+    pmChipFreeCells(chip);
+    psFree(chip->cells);
+
+    psFree(chip->concepts);
+    psFree(chip->analysis);
+    psFree(chip->hdu);
+
+    # if FPA_ASTROM
+
+    psFree(chip->toFPA);
+    psFree(chip->fromFPA);
+    # endif
+}
+
+
+static void FPAFree(pmFPA *fpa)
+{
+    psTrace("psModules.camera", 9, "Freeing fpa %zd\n", (size_t)fpa);
+
+    // NULL the parent pointers
+    psArray *chips = fpa->chips;
+    for (int i = 0 ; i < chips->n ; i++) {
+        pmChip *tmpChip = chips->data[i];
+        if (! tmpChip) {
+            continue;
+        }
+        tmpChip->parent = NULL;
+    }
+    psFree(fpa->chips);
+    psFree(fpa->concepts);
+    psFree(fpa->analysis);
+    psFree((void *)fpa->camera);
+    psFree(fpa->hdu);
+
+    # if FPA_ASTROM
+
+    psFree(fpa->fromTPA);
+    psFree(fpa->toTPA);
+    psFree(fpa->toSky);
+    # endif
+}
+
+void pmCellFreeReadouts(pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell,);
+
+    //
+    // Set the parent to NULL in all cell->readouts before psFree(cell->readouts)
+    // in order to avoid memory reference counter problems.
+    //
+    psArray *readouts = cell->readouts;
+    for (psS32 i = 0 ; i < readouts->n ; i++) {
+        pmReadout *tmpReadout = readouts->data[i];
+        if (! tmpReadout) {
+            continue;
+        }
+        tmpReadout->parent = NULL;
+        psTrace("psModules.camera", 9, "Will now free readout %zd...\n", (size_t)tmpReadout);
+    }
+    cell->readouts = psArrayRealloc(cell->readouts, 0);
+    cell->readouts->n = 0;
+}
+
+
+void pmChipFreeCells(pmChip *chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chip,);
+
+    //
+    // Set the parent to NULL in all chip->cells before psFree(chip->cells)
+    // in order to avoid memory reference counter problems.
+    //
+    psArray *cells = chip->cells;
+    for (int i = 0 ; i < cells->n ; i++) {
+        pmCell *tmpCell = cells->data[i];
+        if (! tmpCell) {
+            continue;
+        }
+        tmpCell->parent = NULL;
+        pmCellFreeReadouts(tmpCell);// Drop all readouts the cell holds
+    }
+    chip->cells = psArrayRealloc(chip->cells, 0);
+    chip->cells->n = 0;
+}
+
+void pmReadoutFreeData (pmReadout *readout)
+{
+    if (!readout) {
+        return;
+    }
+
+    psFree(readout->image);
+    psFree(readout->mask);
+    psFree(readout->weight);
+    psFree(readout->bias);
+
+    psTrace("psModules.camera", 3, "Freeing readout data for %zd\n", (size_t) readout);
+
+    readout->image = NULL;
+    readout->weight = NULL;
+    readout->mask = NULL;
+    readout->bias = psListAlloc(NULL);
+}
+
+void pmCellFreeData(pmCell *cell)
+{
+    if (!cell) {
+        return;
+    }
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadoutFreeData(cell->readouts->data[i]);
+    }
+    if (cell->hdu) {
+        psFree(cell->hdu->images);
+        psFree(cell->hdu->weights);
+        psFree(cell->hdu->masks);
+        // psFree(cell->hdu->header);
+
+        cell->hdu->images = NULL;
+        cell->hdu->weights = NULL;
+        cell->hdu->masks = NULL;
+        // cell->hdu->header = NULL;
+    }
+}
+
+void pmChipFreeData(pmChip *chip)
+{
+    if (!chip) {
+        return;
+    }
+
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCellFreeData(chip->cells->data[i]);
+    }
+    if (chip->hdu) {
+        psFree(chip->hdu->images);
+        psFree(chip->hdu->weights);
+        psFree(chip->hdu->masks);
+        // psFree(chip->hdu->header);
+
+        chip->hdu->images = NULL;
+        chip->hdu->weights = NULL;
+        chip->hdu->masks = NULL;
+        // chip->hdu->header = NULL;
+    }
+}
+
+void pmFPAFreeData(pmFPA *fpa)
+{
+    if (!fpa) {
+        return;
+    }
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChipFreeData(fpa->chips->data[i]);
+    }
+    if (fpa->hdu) {
+        psFree(fpa->hdu->images);
+        psFree(fpa->hdu->weights);
+        psFree(fpa->hdu->masks);
+        // psFree(fpa->hdu->header);
+
+        fpa->hdu->images = NULL;
+        fpa->hdu->weights = NULL;
+        fpa->hdu->masks = NULL;
+        // fpa->hdu->header = NULL;
+    }
+}
+
+pmReadout *pmReadoutAlloc(pmCell *cell)
+{
+    pmReadout *tmpReadout = (pmReadout *)psAlloc(sizeof(pmReadout));
+    psMemSetDeallocator(tmpReadout, (psFreeFunc) readoutFree);
+
+    tmpReadout->image = NULL;
+    tmpReadout->mask = NULL;
+    tmpReadout->weight = NULL;
+    tmpReadout->bias = psListAlloc(NULL);
+    tmpReadout->analysis = psMetadataAlloc();
+    tmpReadout->parent = cell;
+    if (cell) {
+        cell->readouts = psArrayAdd(cell->readouts, 1, (psPtr) tmpReadout);
+    }
+
+    tmpReadout->process = true;            // All cells are processed by default
+    tmpReadout->file_exists = false;       // file not yet identified
+    tmpReadout->data_exists = false;       // data yet read in
+
+    tmpReadout->row0 = 0;
+    tmpReadout->col0 = 0;
+
+    return(tmpReadout);
+}
+
+pmCell *pmCellAlloc(
+    pmChip *chip,
+    const char *name
+)
+{
+    pmCell *tmpCell = (pmCell *) psAlloc(sizeof(pmCell));
+    psMemSetDeallocator(tmpCell, (psFreeFunc) cellFree);
+
+    tmpCell->config = NULL;
+    tmpCell->analysis = psMetadataAlloc();
+    tmpCell->readouts = psArrayAlloc(0);
+    tmpCell->parent = chip;
+    if (chip) {
+        chip->cells = psArrayAdd(chip->cells, 1, (psPtr) tmpCell);
+    }
+    tmpCell->hdu = NULL;
+    tmpCell->process = true;            // All cells are processed by default
+    tmpCell->file_exists = false;       // Not yet identified
+    tmpCell->data_exists = false;       // Not yet read in
+
+    tmpCell->concepts = psMetadataAlloc();
+    tmpCell->conceptsRead = PM_CONCEPT_SOURCE_NONE;
+    if (!psMetadataAddStr(tmpCell->concepts, PS_LIST_HEAD, "CELL.NAME", 0, NULL, name)) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not add CELL.NAME to metadata.\n");
+    }
+    pmConceptsBlankCell(tmpCell);
+
+    return tmpCell;
+}
+
+pmChip *pmChipAlloc(
+    pmFPA *fpa,
+    const char *name)
+{
+    pmChip *tmpChip = (pmChip*)psAlloc(sizeof(pmChip));
+    psMemSetDeallocator(tmpChip, (psFreeFunc) chipFree);
+
+    #if FPA_ASTROM
+
+    tmpChip->toFPA = NULL;
+    tmpChip->fromFPA = NULL;
+    #endif
+
+    tmpChip->analysis = psMetadataAlloc();
+    tmpChip->cells = psArrayAlloc(0);
+    tmpChip->parent = fpa;
+    if (fpa) {
+        psArrayAdd(fpa->chips, 1, (psPtr)tmpChip);
+    }
+    tmpChip->hdu = NULL;
+    tmpChip->process = true;            // Work on all chips, by default
+    tmpChip->file_exists = false;       // Not yet identified
+    tmpChip->data_exists = false;       // Not yet read in
+
+    tmpChip->concepts = psMetadataAlloc();
+    tmpChip->conceptsRead = PM_CONCEPT_SOURCE_NONE;
+    if (!psMetadataAddStr(tmpChip->concepts, PS_LIST_HEAD, "CHIP.NAME", 0, NULL, name)) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not add CHIP.NAME %s to concepts.\n", name);
+    }
+    pmConceptsBlankChip(tmpChip);
+    return tmpChip;
+}
+
+pmFPA *pmFPAAlloc(const psMetadata *camera)
+{
+    pmFPA *tmpFPA = (pmFPA *) psAlloc(sizeof(pmFPA));
+    psMemSetDeallocator(tmpFPA, (psFreeFunc) FPAFree);
+
+    #if FPA_ASTROM
+
+    tmpFPA->fromTPA = NULL;
+    tmpFPA->toTPA = NULL;
+    tmpFPA->toSky = NULL;
+    #endif
+
+    tmpFPA->analysis = psMetadataAlloc();
+    tmpFPA->camera = psMemIncrRefCounter((psPtr)camera);
+    tmpFPA->chips = psArrayAlloc(0);
+    tmpFPA->hdu = NULL;
+
+    tmpFPA->concepts = psMetadataAlloc();
+    tmpFPA->conceptsRead = PM_CONCEPT_SOURCE_NONE;
+    pmConceptsBlankFPA(tmpFPA);
+
+    return tmpFPA;
+}
+
+// Check a cell to ensure that all component readouts have the parent pointer set correctly
+static bool cellCheckParents(pmCell *cell // Cell to check
+                            )
+{
+    PS_ASSERT_PTR_NON_NULL(cell, true);
+
+    bool flag = true;
+    for (long i = 0; i < cell->readouts->n ; i++) {
+        pmReadout *tmpReadout = (pmReadout *) cell->readouts->data[i];
+        if (!tmpReadout) {
+            continue;
+        }
+        if (tmpReadout->parent != cell) {
+            tmpReadout->parent = cell;
+            flag = false;
+        }
+    }
+    return flag;
+}
+
+// Check a chip to ensure that all component cells have the parent pointer set correctly
+static bool chipCheckParents(pmChip *chip // Chip to check
+                            )
+{
+    PS_ASSERT_PTR_NON_NULL(chip, true);
+
+    bool flag = true;
+    for (long i = 0; i < chip->cells->n ; i++) {
+        pmCell *tmpCell = (pmCell*)chip->cells->data[i];
+        if (!tmpCell) {
+            continue;
+        }
+        if (tmpCell->parent != chip) {
+            tmpCell->parent = chip;
+            flag = false;
+        }
+
+        flag &= cellCheckParents(tmpCell);
+    }
+    return flag;
+}
+
+psBool pmFPACheckParents(pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, true);
+
+    bool flag = true;
+    for (long i = 0; i < fpa->chips->n ; i++) {
+        pmChip *tmpChip = (pmChip*)fpa->chips->data[i];
+        if (!tmpChip) {
+            continue;
+        }
+        if (tmpChip->parent != fpa) {
+            tmpChip->parent = fpa;
+            flag = false;
+        }
+
+        flag &= chipCheckParents(tmpChip);
+    }
+    return flag;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPA.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPA.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPA.h	(revision 15358)
@@ -0,0 +1,199 @@
+/* @file pmFPA.h
+ * @brief Defines the focal plane hierarchy, along with functions for interacting with it
+ *
+ * @author George Gusciora, MHPCC
+ * @author Paul Price, IfA
+ * @author Eugene Magnier, IfA
+ *
+ * @version $Revision: 1.18 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-09-21 01:31:51 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_H
+#define PM_FPA_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+#define FPA_ASTROM 1                    ///< Include astrometry information in the structures?
+
+// Return chip position, given FPA position; calculations are all done in pixel units
+#define PM_FPA_TO_CHIP(pos, chip0, chipParity) \
+    (((pos) - (chip0))*(chipParity))
+
+// Return cell position, given chip position; calculations are all done in pixel units
+#define PM_CHIP_TO_CELL(pos, cell0, cellParity, binning) \
+    (((pos) - (cell0))*(cellParity)/(binning))
+
+// Return chip position, given a cell position; calculations are all done in pixel units
+#define PM_CELL_TO_CHIP(pos, cell0, cellParity, binning) \
+    ((pos)*(binning)*(cellParity) + (cell0))
+
+// Return FPA position, given a chip position; calculations are all done in pixel units
+#define PM_CHIP_TO_FPA(pos, chip0, chipParity) \
+    ((pos)*(chipParity) + (chip0))
+
+/// Focal plane array (the entirety of the camera)
+///
+/// The FPA is the top-level camera structure, and consists of one or more chips.  It also contains the
+/// concepts metadata appropriate to this level, a summary of analysis tasks that have been performed, the
+/// camera configuration information, any HDU that corresponds to this level for the file of interest, and
+/// astrometric transformations.  The astrometric transformations encode how to transform from the tangent
+/// plane to the sky, and back.
+typedef struct
+{
+    #if FPA_ASTROM
+    // Astrometric transformations
+    psPlaneTransform *fromTPA;  ///< Transformation from tangent plane to focal plane, or NULL
+    psPlaneTransform *toTPA;  ///< Transformation from focal plane to tangent plane, or NULL
+    psProjection *toSky;         ///< Projection from tangent plane to sky, or NULL
+    #endif
+    // Information
+    psMetadata *concepts;               ///< FPA-level concepts
+    unsigned int conceptsRead;          ///< Which concepts have been read; see pmConceptsSource
+    psMetadata *analysis;               ///< FPA-level analysis metadata
+    const psMetadata *camera;           ///< Camera configuration
+    psArray *chips;                     ///< The component chips
+    pmHDU *hdu;                         ///< FITS header data unit of interest, or NULL
+}
+pmFPA;
+
+/// A chip (contiguous detector element)
+///
+/// The chip is the mid-level camera structure, being part of an FPA, and consisting of one or more cells
+/// (e.g., a CCD).  It also contains the concepts metadata appropriate to this level, a summary of analysis
+/// tasks that have been performed, status flags, any HDU that corresponds to this level for the file of
+/// interest, and astrometric transformations.  The astrometric transformations provide transforms between the
+/// chip and FPA coordinates and back.
+typedef struct
+{
+    #if FPA_ASTROM
+    // Astrometric transformations
+    psPlaneTransform *toFPA;            ///< Transformation from chip to FPA coordinates, or NULL
+    psPlaneTransform *fromFPA;          ///< Transformation from FPA to chip coordinates, or NULL
+    #endif
+    // Information
+    psMetadata *concepts;               ///< Chip-level concepts
+    unsigned int conceptsRead;          ///< Which concepts have been read; see pmConceptsSource
+    psMetadata *analysis;               ///< Chip-level analysis metadata
+    psArray *cells;                     ///< The component cells
+    pmFPA *parent;                      ///< Parent FPA
+    bool process;                       ///< Do we bother about reading and working with this chip?
+    bool file_exists;                   ///< Does the file for this chip exist (read case only)?
+    bool data_exists;                   ///< Does the data for this chip exist (read case only)?
+    pmHDU *hdu;                         ///< FITS header data unit of interest,
+}
+pmChip;
+
+/// A cell (smallest logical unit)
+///
+/// A cell is the lowest-level camera structure, being part of a chip (e.g., an amplifier).  It may consist of
+/// one or more readouts, which are individual reads of the cell.  It also contains the concepts metadata
+/// appropriate to this level, the cell configuration information (for convenience) from the camera
+/// configuration, a summary of analysis tasks that have been performed, status flags, and any HDU that
+/// corresponds to this level for the file of interest
+
+/** Cell data structure
+ *
+ *  A cell consists of one or more readouts.  It also contains a pointer to the
+ *  cell's metadata, and its parent chip.  On the astrometry side, it also
+ *  contains coordinate transforms from the cell to chip, from the cell to
+ *  focal-plane, as well as a "quick and dirty" tranform from the cell to
+ *  sky coordinates.
+ *
+ */
+typedef struct
+{
+    psMetadata *concepts;               ///< Cell-level concepts
+    unsigned int conceptsRead;          ///< Which concepts have been read; see pmConceptsSource
+    psMetadata *config;                 ///< Cell configuration information (from CELLS in the camera config)
+    psMetadata *analysis;               ///< Cell-level analysis metadata
+    psArray *readouts;                  ///< The component readouts
+    pmChip *parent;                     ///< Parent chip
+    bool process;                       ///< Do we bother about reading and working with this cell?
+    bool file_exists;                   ///< Does the file for this cell exist (read case only)?
+    bool data_exists;                   ///< Does the data for this cell exist (read case only)?
+    pmHDU *hdu;                         ///< FITS header data unit of interest
+}
+pmCell;
+
+/// A readout (individual read of a cell)
+///
+/// A readout corresponds to an individual read of a cell (e.g., a single image as part of a video sequence,
+/// or one of multiple coadds).  It contains the actual pixels used in analysis (along with mask and weight
+/// maps).  When reading from a FITS file, the images are subimages (from CELL.TRIMSEC) of the pixels read
+/// from the appropriate HDU (at the FPA, chip or cell level).  The readout also contains a list of bias
+/// sections (prescans or overscans, or otherwise), a summary of analysis tasks that have been performed,
+/// status flags, and the offsets used for reading a FITS file incrementally.
+typedef struct
+{
+    int col0;                           ///< Column offset; non-zero if reading in columns incrementally
+    int row0;                           ///< Row offset; non-zero if reading in rows incrementally
+    psImage *image;                     ///< Imaging area of readout (corresponds to CELL.TRIMSEC region)
+    psImage *mask;                      ///< Mask of input image (corresponds to CELL.TRIMSEC region)
+    psImage *weight;                    ///< Weight of input image (corresponds to CELL.TRIMSEC region)
+    psList *bias;                       ///< List of bias (prescan/overscan) images
+    psMetadata *analysis;               ///< Readout-level analysis metadata
+    pmCell *parent;                     ///< Parent cell
+    bool process;                       ///< Do we bother about reading and working with this readout?
+    bool file_exists;                   ///< Does the file for this readout exist (read case only)?
+    bool data_exists;                   ///< Does the data for this readout exist (read case only)?
+}
+pmReadout;
+
+/// Free all readouts within a cell
+void pmCellFreeReadouts(pmCell *cell    ///< Cell for which to free readouts
+                       );
+
+/// Free all cells within a chip
+void pmChipFreeCells(pmChip *chip       ///< Chip for which to free cells
+                    );
+
+/// Free all data within a readout
+void pmReadoutFreeData(pmReadout *readout ///< Readout for which to free data
+                      );
+
+/// Free all data within a cell (all readouts as well as metadata)
+void pmCellFreeData(pmCell *cell        ///< Cell for which to free data
+                   );
+
+/// Free all data within a chip (all cells as well as metadata)
+void pmChipFreeData(pmChip *chip        ///< Chip for which to free data
+                   );
+
+/// Free all data within an FPA (all chips as well as metadata)
+void pmFPAFreeData(pmFPA *fpa           ///< FPA for which to free data
+                  );
+
+/// Allocate a readout associated with a cell
+pmReadout *pmReadoutAlloc(pmCell *cell  ///< Parent cell, or NULL
+                         );
+
+/// Allocate a cell associated with a chip
+///
+/// The name is used to set CELL.NAME within the concepts.
+pmCell *pmCellAlloc(pmChip *chip,       ///< Parent chip, or NULL
+                    const char *name    ///< Name of cell, for CELL.NAME
+                   );
+
+/// Allocate a chip associated with an FPA
+///
+/// The name is used to set CHIP.NAME within the concepts
+pmChip *pmChipAlloc(pmFPA *fpa,         ///< Parent FPA, or NULL
+                    const char *name    ///< Name of chip, for CHIP.NAME
+                   );
+
+/// Allocate an FPA
+pmFPA *pmFPAAlloc(const psMetadata *camera ///< Camera configuration (to store in FPA)
+                 );
+
+
+/// Check parent links within an FPA
+///
+/// Iterates through the FPA to verify that the "parent" links in the chip, cell and readout are set
+/// correctly.  If there are any incorrect links, they are fixed, and the function returns false.
+bool pmFPACheckParents(pmFPA *fpa       ///< FPA to check
+                      );
+/// @}
+#endif // #ifndef PM_FPA_H
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAAstrometry.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAAstrometry.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAAstrometry.c	(revision 15358)
@@ -0,0 +1,512 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "pmFPAAstrometry.h"
+#include "pmFPA.h"
+
+
+/*****************************************************************************
+checkValidImageCoords(): this is a private function which simply determines if
+the supplied x,y coordinates are in the range for the supplied psImage.
+ 
+XXX: What about col0 and row0
+XXX: This should return a psBool.
+XXX: Macro this for speed.
+ *****************************************************************************/
+static psS32 checkValidImageCoords(
+    double x,
+    double y,
+    psImage* tmpImage)
+{
+    PS_ASSERT_IMAGE_NON_NULL(tmpImage, 0);
+
+    // The FLT_EPSILON is because -0.0 was failing this.
+    if (((x+FLT_EPSILON) < 0.0) || (x > (double)tmpImage->numCols) ||
+            ((y+FLT_EPSILON) < 0.0) || (y > (double)tmpImage->numRows)) {
+        return (0);
+    }
+
+    return (1);
+}
+
+/*****************************************************************************/
+/* FUNCTION IMPLEMENTATION - PUBLIC                                          */
+/*****************************************************************************/
+
+pmCell* pmCellInFPA(
+    const psPlane* fpaCoord,
+    const pmFPA* FPA)
+{
+    PS_ASSERT_PTR_NON_NULL(fpaCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(FPA, NULL);
+
+    pmChip* tmpChip = NULL;
+    psPlane chipCoord;
+    pmCell* outCell = NULL;
+
+    // Determine which chip contains the fpaCoords.
+    tmpChip = pmChipInFPA(fpaCoord, FPA);
+    if (tmpChip == NULL) {
+        return(NULL);
+    }
+
+    // Convert to those chip coordinates.
+    psPlane *rc = pmCoordFPAToChip(&chipCoord, fpaCoord, tmpChip);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine Chip coords.\n");
+        return(NULL);
+    }
+
+    // Determine which cell contains those chip coordinates.
+    outCell = pmCellInChip(&chipCoord, tmpChip);
+    if (outCell == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the cell.\n");
+        return(NULL);
+    }
+
+    return (outCell);
+}
+
+pmChip* pmChipInFPA(
+    const psPlane* fpaCoord,
+    const pmFPA* FPA)
+{
+    PS_ASSERT_PTR_NON_NULL(fpaCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(FPA, NULL);
+    PS_ASSERT_PTR_NON_NULL(FPA->chips, NULL);
+
+    psArray* chips = FPA->chips;
+    psS32 nChips = chips->n;
+    psPlane chipCoord;
+    pmCell *tmpCell = NULL;
+
+    //
+    // Loop through every chip in this FPA.  Convert the original FPA
+    // coordinates to chip coordinates for that chip.  Then, determine if any
+    // cells in that chip contain those chip coordinates.
+    // XXX: Depending on the number of chips, and their topology, there may be
+    // a much more efficient way of doing this.
+    //
+    for (psS32 i = 0; i < nChips; i++) {
+        pmChip* tmpChip = chips->data[i];
+        PS_ASSERT_PTR_NON_NULL(tmpChip, NULL);
+        PS_ASSERT_PTR_NON_NULL(tmpChip->fromFPA, NULL);
+
+        psPlaneTransformApply(&chipCoord, tmpChip->fromFPA, fpaCoord);
+
+        tmpCell = pmCellInChip(&chipCoord, tmpChip);
+        if (tmpCell != NULL) {
+            return(tmpChip);
+        }
+    }
+
+    psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the chip.\n");
+    return (NULL);
+}
+
+
+pmCell* pmCellInChip(
+    const psPlane* chipCoord,
+    const pmChip* chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chipCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    psPlane cellCoord;
+    psArray* cells;
+
+    cells = chip->cells;
+    if (cells == NULL) {
+        return NULL;
+    }
+
+    //
+    // We loop over each cell in the chip.  We transform the chipCoord into
+    // a cellCoord for that cell and determine if that cellCoord is valid.
+    // If so, then we return that cell.
+    // XXX: Depending on the number of cells, and their topology, there may be
+    // a much more efficient way of doing this.
+    //
+    for (psS32 i = 0; i < cells->n; i++) {
+        pmCell* tmpCell = (pmCell* ) cells->data[i];
+        PS_ASSERT_PTR_NON_NULL(tmpCell, NULL);
+
+        psPlaneTransform *chipToCell = NULL;
+        if (true ==  p_psIsProjectionLinear(tmpCell->toChip)) {
+            chipToCell = p_psPlaneTransformLinearInvert(tmpCell->toChip);
+        } else {
+            psLogMsg(__func__, PS_LOG_WARN, "WARNING: non-linear cell->chip transforms are not yet implemented.\n");
+            //chipToCell = psPlaneTransformInvert(NULL, tmpCell->toChip, NULL, -1);
+            chipToCell = NULL;
+        }
+        if (chipToCell == NULL) {
+            psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not invert the Cell->toChip transform.\n");
+            return(NULL);
+        }
+        psArray* readouts = tmpCell->readouts;
+
+        if (readouts != NULL) {
+            for (psS32 j = 0; j < readouts->n; j++) {
+                pmReadout* tmpReadout = readouts->data[j];
+                PS_ASSERT_READOUT_NON_NULL(tmpReadout, NULL);
+
+                psPlaneTransformApply(&cellCoord,
+                                      chipToCell,
+                                      chipCoord);
+
+                if (checkValidImageCoords(cellCoord.x,
+                                          cellCoord.y,
+                                          tmpReadout->image)) {
+                    psFree(chipToCell);
+                    return (tmpCell);
+                }
+            }
+        }
+        psFree(chipToCell);
+    }
+
+    //psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the cell.\n");
+    return (NULL);
+}
+
+
+psPlane* pmCoordCellToFPA(
+    psPlane* fpaCoord,
+    const psPlane* cellCoord,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cellCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+
+    psPlane *rc = psPlaneTransformApply(fpaCoord, cell->toFPA, cellCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform cell coords to FPA coords.\n");
+    }
+    return(rc);
+}
+
+
+psPlane* pmCoordChipToFPA(
+    psPlane* outCoord,
+    const psPlane* inCoord,
+    const pmChip* chip)
+{
+    PS_ASSERT_PTR_NON_NULL(inCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    psPlane *rc = psPlaneTransformApply(outCoord, chip->toFPA, inCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform chip coords to FPA coords.\n");
+    }
+    return(rc);
+}
+
+
+psPlane* pmCoordFPAToChip(
+    psPlane* chipCoord,
+    const psPlane* fpaCoord,
+    const pmChip* chip)
+{
+    PS_ASSERT_PTR_NON_NULL(fpaCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip->fromFPA, NULL);
+
+    psPlane *rc = psPlaneTransformApply(chipCoord, chip->fromFPA, fpaCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform FPA coords to Chip coords.\n");
+    }
+    return(rc);
+}
+
+psPlane* pmCoordCellToChip(
+    psPlane* outCoord,
+    const psPlane* inCoord,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(inCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+
+    psPlane *rc = psPlaneTransformApply(outCoord, cell->toChip, inCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform Cell coords to Chip coords.\n");
+    }
+    return(rc);
+}
+
+psPlane* pmCoordChipToCell(
+    psPlane* cellCoord,
+    const psPlane* chipCoord,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(chipCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent, NULL);
+
+    pmCell *tmpCell = pmCellInChip(chipCoord, cell->parent);
+    if (tmpCell == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the proper cell.\n");
+        return(NULL);
+    }
+
+    psPlaneTransform *tmpChipToCell = NULL;
+    PS_ASSERT_PTR_NON_NULL(tmpCell->toChip, NULL);
+    if (true ==  p_psIsProjectionLinear(tmpCell->toChip)) {
+        tmpChipToCell = p_psPlaneTransformLinearInvert(tmpCell->toChip);
+    } else {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: non-linear cell->chip transforms are not yet implemented.\n");
+        // XXX: tmpChipToCell = psPlaneTransformInvert(NULL, tmpCell->toChip, NULL, -1);
+        tmpChipToCell = NULL;
+    }
+    if (tmpChipToCell == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not invert the Cell->toChip transform.\n");
+        return(NULL);
+    }
+
+    psPlane *rc = psPlaneTransformApply(cellCoord, tmpChipToCell, chipCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform Chip coords to Cell coords.\n");
+    }
+    psFree(tmpChipToCell);
+    return(rc);
+}
+
+psPlane* pmCoordFPAToTP(
+    psPlane* outCoord,
+    const psPlane* inCoord,
+    double color,
+    double magnitude,
+    const pmFPA* fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(inCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    psPlane *rc = psPlaneDistortApply(outCoord, fpa->toTangentPlane, inCoord, color, magnitude);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform FPA coords to tangent plane coords.\n");
+    }
+    return(rc);
+}
+
+psPlane* pmCoordTPToFPA(
+    psPlane* fpaCoord,
+    const psPlane* tpCoord,
+    double color,
+    double magnitude,
+    const pmFPA* fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(tpCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa->fromTangentPlane, NULL);
+
+    psPlane *rc = psPlaneDistortApply(fpaCoord, fpa->fromTangentPlane, tpCoord, color, magnitude);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform tangent plane coords to FPA coords.\n");
+    }
+    return(rc);
+}
+
+
+/*****************************************************************************
+XXXDeproject(outSphere, coord, projection): This private routine is a wrapper
+for p_psDeproject().  The reason: p_psDeproject() and p_psProject() combined
+do not seem to produce the original coordinates when they even though they
+should.  XXXDeproject() simply negates the ->r and ->d members of the output
+psSphere if the input ->y is larger than 0.0.  I don't know why it works.
+ 
+I'm guessing the p_psProject() and p_psDeproject() functions have bugs.
+ 
+XXX: It appears that p_psProject() and p_psDeproject() have been fixed.
+Remove this.
+ *****************************************************************************/
+psSphere* XXXDeproject(
+    psSphere *outSphere,
+    const psPlane* coord,
+    const psProjection* projection)
+{
+    psSphere *rc = p_psDeproject(outSphere, coord, projection);
+
+    if (coord->y >= 0.0) {
+        rc->d = -rc->d;
+        rc->r = -rc->r;
+    }
+
+    return(rc);
+}
+
+/*****************************************************************************
+  *****************************************************************************/
+psSphere* pmCoordTPToSky(
+    psSphere* outSphere,
+    const psPlane* tpCoord,
+    const psProjection *projection)
+{
+    PS_ASSERT_PTR_NON_NULL(tpCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(projection, NULL);
+
+    //    psSphere *rc = XXXDeproject(outSphere, tpCoord, projection);
+    psSphere *rc = p_psDeproject(outSphere, tpCoord, projection);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform tangent plane coords to sky coords.\n");
+    }
+    return(rc);
+}
+
+/*****************************************************************************
+ *****************************************************************************/
+psPlane* pmCoordSkyToTP(
+    psPlane* tpCoord,
+    const psSphere* in,
+    const psProjection *projection)
+{
+    PS_ASSERT_PTR_NON_NULL(in, NULL);
+    PS_ASSERT_PTR_NON_NULL(projection, NULL);
+
+    psPlane *rc = p_psProject(tpCoord, in, projection);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform sky to tangent plane coords.\n");
+    }
+    return(rc);
+}
+
+/*****************************************************************************
+ *****************************************************************************/
+psSphere* pmCoordCellToSky(
+    psSphere* skyCoord,
+    const psPlane* cellCoord,
+    double color,
+    double magnitude,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cellCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->toFPA, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent->parent, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent->parent->toTangentPlane, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent->parent->projection, NULL);
+    psPlane fpaCoord;
+    psPlane tpCoord;
+    psPlane *rc;
+    pmFPA* parFPA = (cell->parent)->parent;
+
+    // Convert the input cell coordinates to FPA coordinates.
+    rc = psPlaneTransformApply(&fpaCoord, cell->toFPA, cellCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could transform cell coords to FPA coords.\n");
+        return(NULL);
+    }
+
+    // Convert the FPA coordinates to tangent plane Coordinates.
+    rc = psPlaneDistortApply(&tpCoord, parFPA->toTangentPlane, &fpaCoord, color, magnitude);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could transform FPA coords to tangent plane coords.\n");
+        return(NULL);
+    }
+
+    // Convert the tangent plane Coordinates to sky coordinates.
+    psSphere *rc2 = pmCoordTPToSky(skyCoord, &tpCoord, parFPA->projection);
+    if (rc2 == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform cell coords to sky coords.\n");
+    }
+
+    return(rc2);
+}
+
+/*****************************************************************************
+ *****************************************************************************/
+psPlane* pmCoordSkyToCell(
+    psPlane* cellCoord,
+    const psSphere* skyCoord,
+    float color,
+    float magnitude,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(skyCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent->parent, NULL);
+    pmChip *parChip = cell->parent;
+    pmFPA *parFPA = parChip->parent;
+    psPlane tpCoord;
+    psPlane fpaCoord;
+    psPlane chipCoord;
+    psPlane *rc;
+
+    // Convert the skyCoords to tangent plane coords.
+    rc = pmCoordSkyToTP(&tpCoord, skyCoord, parFPA->projection);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine tangent plane coords.\n");
+        return(NULL);
+    }
+
+    // Convert the tangent plane coords to FPA coords.
+    rc = pmCoordTPToFPA(&fpaCoord, &tpCoord, color, magnitude, parFPA);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine FPA coords.\n");
+        return(NULL);
+    }
+
+    // Convert the FPA coords to chip coords.
+    rc = pmCoordFPAToChip(&chipCoord, &fpaCoord, parChip);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine chip coords.\n");
+        return(NULL);
+    }
+
+    // Convert the chip coords to cell coords.
+    rc = pmCoordChipToCell(cellCoord, &chipCoord, cell);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine cell coords.\n");
+        return(NULL);
+    }
+
+    return (cellCoord);
+}
+
+/*****************************************************************************
+ *****************************************************************************/
+psSphere* pmCoordCellToSkyQuick(
+    psSphere* outSphere,
+    const psPlane* cellCoord,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cellCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->toSky, NULL);
+    psPlane outPlane;
+    psPlane *rc;
+    rc = psPlaneTransformApply(&outPlane, cell->toSky, cellCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could transform cell coords to sky coords.\n");
+        return(NULL);
+    }
+
+    psSphere *out = outSphere;
+    if (out == NULL) {
+        out = psSphereAlloc();
+    }
+    out->r = outPlane.y;
+    out->d = outPlane.x;
+
+    return(out);
+}
+
+/*****************************************************************************
+ *****************************************************************************/
+psPlane* pmCoordSkyToCellQuick(
+    psPlane* cellCoord,
+    const psSphere* skyCoord,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(skyCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->toSky, NULL);
+    psPlane skyPlane;
+    skyPlane.y = skyCoord->r;
+    skyPlane.x = skyCoord->d;
+
+    psPlane *rc = psPlaneTransformApply(cellCoord, cell->toSky, &skyPlane);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform sky to cell coords.\n");
+    }
+    return(cellCoord);
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAAstrometry.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAAstrometry.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAAstrometry.h	(revision 15358)
@@ -0,0 +1,188 @@
+#ifndef PM_FPA_ASTROMETRY_H
+#define PM_FPA_ASTROMETRY_H
+
+#include "pslib.h"
+#include "pmFPA.h"
+
+/** Find cooresponding cell for given FPA coordinate
+ *
+ *  @return pmCell*    the cell cooresponding to the coord in FPA
+ */
+pmCell* pmCellInFPA(
+    const psPlane* coord,              ///< the coordinate in FPA plane
+    const pmFPA* FPA                   ///< the FPA to search for the cell
+);
+
+
+/** Find cooresponding chip for given FPA coordinate
+ *
+ *  @return pmChip*    the chip cooresponding to coord
+ */
+pmChip* pmChipInFPA(
+    const psPlane* coord,              ///< the coordinate in FPA plane
+    const pmFPA* FPA                   ///< the FPA to search for the cell
+);
+
+
+/** Find cooresponding cell for given Chip coordinate
+ *
+ *  @return pmCell*    the cell cooresponding to coord
+ */
+pmCell* pmCellInChip(
+    const psPlane* coord,              ///< the coordinate in Chip plane
+    const pmChip* chip                 ///< the chip to search for the cell
+);
+
+
+/** Translate a cell coordinate into a chip coordinate
+ *
+ *  @return psPlane*    the resulting chip coordinate
+ */
+psPlane* pmCoordCellToChip(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within Cell
+    const pmCell* cell                 ///< the Cell in interest
+);
+
+
+/** Translate a chip coordinate into a FPA coordinate
+ *
+ *  @return psPlane*    the resulting FPA coordinate
+ */
+psPlane* pmCoordChipToFPA(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within Chip
+    const pmChip* chip                 ///< the chip in interest
+);
+
+
+/** Translate a FPA coordinate into a Tangent Plane coordinate
+ *
+ *  @return psPlane*    the resulting Tangent Plane coordinate
+ */
+psPlane* pmCoordFPAToTP(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within FPA
+    double color,                      ///< Color of source
+    double magnitude,                  ///< Magnitude of source
+    const pmFPA* fpa                   ///< the FPA in interest
+);
+
+
+/** Translate a Tangent Plane coordinate into a Sky coordinate
+ *
+ *  @return psSphere*    the resulting Sky coordinate
+ */
+psSphere* pmCoordTPToSky(
+    psSphere* out,                     ///< a sphere struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                ///< the coordinate within Tangent Plane
+    const psProjection *projection
+);
+
+/** Translate a cell coordinate into a FPA coordinate
+ *
+ *  @return psPlane*    the resulting FPA coordinate
+ */
+psPlane* pmCoordCellToFPA(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within cell
+    const pmCell* cell                 ///< the cell in interest
+);
+
+
+/** Translate a cell coordinate into a Sky coordinate
+ *
+ *  @return psSphere*    the resulting Sky coordinate
+ */
+psSphere* pmCoordCellToSky(
+    psSphere* out,                     ///< a sphere struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within cell
+    double color,                      ///< Color of source
+    double magnitude,                  ///< Magnitude of source
+    const pmCell* cell                 ///< the cell in interest
+);
+
+
+/** Translate a cell coordinate into a Sky coordinate using a 'quick and
+ *  dirty' method
+ *
+ *  @return psSphere*    the resulting Sky coordinate
+ */
+psSphere* pmCoordCellToSkyQuick(
+    psSphere* out,                     ///< a sphere struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within cell
+    const pmCell* cell                 ///< the cell in interest
+);
+
+
+/** Translate a Sky coordinate into a Tangent Plane coordinate
+ *
+ *  @return psPlane*    the resulting Tangent Plane coordinate
+ */
+psPlane* pmCoordSkyToTP(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psSphere* in,                ///< the sky coordinate
+    const psProjection *projection
+);
+
+/** Translate a Tangent Plane coordinate into a FPA coordinate
+ *
+ *  @return psPlane*    the resulting FPA coordinate
+ */
+psPlane* pmCoordTPToFPA(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within tangent plane
+    double color,                      ///< Color of source
+    double magnitude,                  ///< Magnitude of source
+    const pmFPA* fpa                   ///< the FPA of interest
+);
+
+
+/** Translate a FPA coordinate into a chip coordinate
+ *
+ *  @return psPlane*    the resulting chip coordinate
+ */
+psPlane* pmCoordFPAToChip(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the FPA coordinate
+    const pmChip* chip                 ///< the chip of interest
+);
+
+
+/** Translate a chip coordinate into a cell coordinate
+ *
+ *  @return psPlane*    the resulting cell coordinate
+ */
+psPlane* pmCoordChipToCell(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the Chip coordinate
+    const pmCell* cell                 ///< the cell of interest
+);
+
+
+/** Translate a sky coordinate into a cell coordinate
+ *
+ *  @return psPlane*    the resulting cell coordinate
+ */
+psPlane* pmCoordSkyToCell(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psSphere* in,                ///< the Sky coordinate
+    float color,                       ///< Color of source
+    float magnitude,                   ///< Magnitude of source
+    const pmCell* cell                 ///< the cell of interest
+);
+
+
+/** Translate a sky coordinate into a cell coordinate using a 'quick and
+ *  dirty' method
+ *
+ *  @return psPlane*    the resulting cell coordinate
+ */
+psPlane* pmCoordSkyToCellQuick(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psSphere* in,                ///< the Sky coordinate
+    const pmCell* cell                 ///< the cell of interest
+);
+
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPACalibration.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPACalibration.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPACalibration.c	(revision 15358)
@@ -0,0 +1,63 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmFPACalibration.h"
+
+float pmFPADarkNorm(const pmFPA *fpa, const pmFPAview *view, float expTime)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NAN);
+    PS_ASSERT_PTR_NON_NULL(view, NAN);
+
+    psMetadata *darkNorms = psMetadataLookupMetadata(NULL, fpa->camera, "DARK.NORM"); // Dark normalisations
+    if (!darkNorms) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find DARK.NORM in camera configuration.");
+        return NAN;
+    }
+
+    const char *key = psMetadataLookupStr(NULL, fpa->camera, "DARK.NORM.KEY"); // Key for normalisation
+    if (!key || strlen(key) == 0) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find DARK.NORM.KEY in camera configuration.");
+        return NAN;
+    }
+
+    psString keyResolved = pmFPANameFromRule(key, fpa, view); // Resolved version
+    if (!keyResolved || strlen(keyResolved) == 0) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to resolve DARK.NORM.KEY: %s", key);
+        return NAN;
+    }
+
+    psMetadata *polyMD = psMetadataLookupMetadata(NULL, darkNorms, keyResolved); // Metadata with polynomial
+    if (!polyMD) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find %s polynomial in the DARK.NORM metadata", keyResolved);
+        psFree(keyResolved);
+        return NAN;
+    }
+
+    psPolynomial1D *poly = psPolynomial1DfromMetadata(polyMD); // Polynomial
+    if (!poly) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to determine polynomial from %s in the DARK.NORM metadata",
+                keyResolved);
+        psFree(keyResolved);
+        return NAN;
+    }
+    psFree(keyResolved);
+
+    float value = psPolynomial1DEval(poly, expTime);
+
+    psFree(poly);
+
+    return value;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPACalibration.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPACalibration.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPACalibration.h	(revision 15358)
@@ -0,0 +1,17 @@
+#ifndef PM_FPA_CALIBRATION_H
+#define PM_FPA_CALIBRATION_H
+
+/// Return the dark normalisation value
+///
+/// Unfortunately, dark current is not linear with the exposure time, but application of a polynomial
+/// correction to the exposure time should make it linear.  This function returns the appropriate value with
+/// which to normalise a dark frame.  The polynomial is obtained from DARK.NORM in the camera configuration.
+/// The specific polynomial metadata to use is provided by DARK.NORM.KEY, which is keyword expanded in the
+/// usual manner (e.g., try "{CHIP.NAME}").
+float pmFPADarkNorm(const pmFPA *fpa,   ///< FPA for which to get the normalisation
+                    const pmFPAview *view, ///< View to the FPA component of interest
+                    float expTime       ///< The nominal exposure time
+    );
+
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAConstruct.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAConstruct.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAConstruct.c	(revision 15358)
@@ -0,0 +1,1512 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <strings.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAFlags.h"
+#include "pmConcepts.h"
+#include "pmFPAConstruct.h"
+#include "pmFPAUtils.h"
+#include "pmHDUUtils.h"
+
+#define TABLE_OF_CONTENTS "CONTENTS"    // Name for camera format metadata containing the contents
+#define CHIP_TYPES "CHIPS"              // Name for camera format metadata containing the chip types
+#define CELL_TYPES "CELLS"              // Name for camera format metadata containing the cell types
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Read data for a particular cell from the camera format description
+static psMetadata *getCellData(const psMetadata *format, // The camera format description
+                               const char *cellName // The name of the cell
+                              )
+{
+    assert(format);
+    assert(cellName && strlen(cellName) > 0);
+
+    bool status = true;                 // Result of MD lookup
+    psMetadata *cells = psMetadataLookupMetadata(&status, format, CELL_TYPES); // The CELLS
+    if (!status || !cells) {
+        psError(PS_ERR_IO, true, "Unable to find %s in camera format.\n", CELL_TYPES);
+        return NULL;
+    }
+
+    psMetadata *cellData = psMetadataLookupMetadata(&status, cells, cellName); // The data for the particular cell
+    if (!status || !cellData) {
+        psLogMsg(__func__, PS_LOG_WARN, "Unable to find specs for cell %s: ignored\n", cellName);
+    }
+
+    return cellData;
+}
+
+// Parse a list of first:second:third pairs in a string
+static int parseContent(psArray **first, // Array of the first values
+                         psArray **second, // Array of the second values
+                         psArray **third, // Array of the third values
+                         const char *string // The string to parse
+                        )
+{
+    assert(string && strlen(string) > 0);
+    // Must populate 'first', 'second', 'third' in order.
+    assert(!second || first);
+    assert(!third || second);
+
+    int numArrays = third ? 3 : (second ? 2 : 1); // Number of arrays
+
+    psList *values = psStringSplit(string, " ,;", true); // List of the parts
+
+    if (first) {
+        *first = psArrayAlloc(values->n);
+    }
+    if (second) {
+        *second = psArrayAlloc(values->n);
+    }
+    if (third) {
+        *third = psArrayAlloc(values->n);
+    }
+    int num = 0;
+    psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false); // Iterator for values
+    psString value = NULL;               // "first:second:third" string
+    while ((value = psListGetAndIncrement(valuesIter))) {
+        psArray *fst = psStringSplitArray(value, ":", true); // First, second, third
+        switch (numArrays) {
+          case 3:
+            psArraySet(*third, num, fst->data[2]);
+          case 2:
+            psArraySet(*second, num, fst->data[1]);
+          case 1:
+            psArraySet(*first, num, fst->data[0]);
+            break;
+        default:
+          psAbort("Should never get here.");
+        }
+        num++;
+        psFree(fst);
+    }
+    psFree(valuesIter);
+    psFree(values);
+
+    return num;
+}
+
+// Get the name of a PHU chip or cell from the header
+static psString phuNameFromHeader(const char *name, // The name to lookup: "CELL.NAME" or "CHIP.NAME"
+                                  const psMetadata *fileInfo, // FILE within the camera format description
+                                  const psMetadata *header // Primary header
+                                 )
+{
+    assert(name && strlen(name) > 0);
+    assert(fileInfo);
+    assert(header);
+
+    bool mdok = true;                   // Result of MD lookup
+    psString keyword = psMetadataLookupStr(&mdok, fileInfo, name);
+    if (!mdok || strlen(keyword) == 0) {
+        return false;
+    }
+    psMetadataItem *resultItem = psMetadataLookup(header, keyword);
+    if (!resultItem) {
+        psError(PS_ERR_IO, true, "Unable to find %s in primary header to identify %s.\n", keyword, name);
+        return NULL;
+    }
+    return psMetadataItemParseString(resultItem);
+}
+
+// Add an HDU to the FPA
+static bool addHDUtoFPA(pmFPA *fpa,     // FPA to which to add
+                        pmHDU *hdu      // HDU to be added
+                       )
+{
+    assert(fpa);
+    assert(hdu);
+
+    if (fpa->hdu) {
+        // Something's already here
+        if (fpa->hdu != hdu) {
+            psError(PS_ERR_IO, true, "Unable to add HDU since FPA already has one.\n");
+        }
+        return false;
+    }
+    fpa->hdu = psMemIncrRefCounter(hdu);
+    pmFPASetFileStatus(fpa, true);
+
+    return true;
+}
+
+// Add an HDU to the chip
+static bool addHDUtoChip(pmChip *chip,  // Chip to which to add
+                         pmHDU *hdu     // HDU to be added
+                        )
+{
+    assert(chip);
+    assert(hdu);
+
+    if (chip->hdu) {
+        // Something's already here
+        if (chip->hdu != hdu) {
+            psError(PS_ERR_IO, true, "Unable to add HDU since chip already has one.\n");
+        }
+        return false;
+    }
+    chip->hdu = psMemIncrRefCounter(hdu);
+    pmChipSetFileStatus(chip, true);
+
+    return true;
+}
+
+// Add an HDU to the cell
+static bool addHDUtoCell(pmCell *cell,  // Cell to which to add
+                         pmHDU *hdu     // HDU to be added
+                        )
+{
+    assert(cell);
+    assert(hdu);
+
+    if (cell->hdu) {
+        // Something's already here
+        if (cell->hdu != hdu) {
+            psError(PS_ERR_IO, true, "Unable to add HDU since cell already has one.\n");
+        }
+        return false;
+    }
+    cell->hdu = psMemIncrRefCounter(hdu);
+    pmCellSetFileStatus(cell, true);
+
+    return true;
+}
+
+
+// Looks up the particular content, based on the chip and cell
+static const char *getContent(const psMetadata *fileInfo, // The FILE from the camera format configuration
+                              const psMetadata *header, // The FITS header
+                              const psMetadata *contents // The CONTENTS from the camera format configuration
+                             )
+{
+    assert(fileInfo);
+    assert(contents);
+    assert(header);
+
+    const char *contentHeader = psMetadataLookupStr(NULL, fileInfo, "CONTENT"); // Keyword to get contents
+    if (!contentHeader || strlen(contentHeader) == 0) {
+        psError(PS_ERR_UNEXPECTED_NULL, false,
+                "Unable to find CONTENT in FILE within camera format configuration.\n");
+        return NULL;
+    }
+
+    psMetadataItem *contentKey = psMetadataLookup(header, contentHeader); // Key to CONTENTS menu
+    if (!contentKey) {
+        psError(PS_ERR_UNEXPECTED_NULL, false,
+                "Unable to find %s in header to determine file content.", contentHeader);
+        return NULL;
+    }
+
+    psString contentKeyStr = psMetadataItemParseString(contentKey); // Key, as a string
+
+    psTrace("psModules.camera", 5, "Looking up %s in the CONTENTS.\n", contentKeyStr);
+    const char *content = psMetadataLookupStr(NULL, contents, contentKeyStr);
+    if (!content || strlen(content) == 0) {
+        psError(PS_ERR_IO, false, "Unable to find %s in the CONTENTS.\n", contentKeyStr);
+        return NULL;
+    }
+
+    psFree(contentKeyStr);
+
+    return content;
+}
+
+
+// Given a list of contents, put the HDU in the correct place and plug in the cell configuration information
+static bool processContents(pmFPA *fpa,  // The FPA
+                            pmChip *chip, // The chip
+                            pmHDU *hdu,  // The HDU to be added
+                            pmFPALevel level, // The level at which to add the HDU
+                            psArray *chipNames, // The chip names
+                            psArray *cellNames, // The cell names
+                            psArray *cellTypes, // The cell types
+                            const psMetadata *format // Camera format configuration
+                            )
+{
+    assert(fpa);
+    assert(cellTypes);
+    long num = cellTypes->n;            // Number of entries to add
+    assert(chip || (chipNames && chipNames->n == num));
+    assert(cellNames && cellNames->n == num);
+    assert(format);
+
+    if (hdu && level == PM_FPA_LEVEL_FPA) {
+        if (!addHDUtoFPA(fpa, hdu)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to add HDU to FPA");
+            return false;
+        }
+    }
+    // Load fpa-related concepts
+    if (!pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS, false, NULL)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts from camera and defaults for fpa\n");
+        return false;
+    }
+
+    for (int i = 0; i < num; i++) {
+        psString cellType = cellTypes->data[i]; // The type of the cell
+
+        // Find the chip
+        pmChip *newChip;                // Chip of interest
+        if (chip) {
+            newChip = chip;
+        } else {
+            psString chipName = chipNames->data[i]; // The name of the chip
+            int chipNum = pmFPAFindChip(fpa, chipName); // The chip we're looking for
+            if (chipNum == -1) {
+                psError(PS_ERR_LOCATION_INVALID, false,
+                        "Unable to find chip %s in fpa --- ignored.\n", chipName);
+                return false;
+            }
+            newChip = fpa->chips->data[chipNum];
+        }
+
+        // Put in the HDU
+        if (hdu && level == PM_FPA_LEVEL_CHIP) {
+            addHDUtoChip(newChip, hdu);
+        }
+        // Load chip-related concepts
+        if (!pmConceptsReadChip(newChip, PM_CONCEPT_SOURCE_DEFAULTS, false, false, NULL)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to read concepts from camera and defaults for chip\n");
+            return false;
+        }
+
+        // Find the cell
+        pmCell *newCell;                // Cell of interest
+        psString cellName = cellNames->data[i]; // The name of the cell
+        int cellNum = pmChipFindCell(newChip, cellName); // The cell we're looking for
+        if (cellNum == -1) {
+            psError(PS_ERR_LOCATION_INVALID, false, "Unable to find cell %s in chip --- ignored.\n",
+                    cellName);
+            return false;
+        }
+        newCell = newChip->cells->data[cellNum];
+
+        psMetadata *cellData = getCellData(format, cellType); // Data for this cell
+
+        if (hdu && level == PM_FPA_LEVEL_CELL) {
+            addHDUtoCell(newCell, hdu);
+        }
+
+        // Put in the cell data
+        if (newCell->config) {
+            psLogMsg(__func__, PS_LOG_WARN, "Overwriting cell data in chip\n");
+            psFree(newCell->config); // Make way!
+        }
+        newCell->config = psMemIncrRefCounter(cellData);
+        if (!pmConceptsReadCell(newCell, PM_CONCEPT_SOURCE_CELLS | PM_CONCEPT_SOURCE_DEFAULTS,
+                                false, NULL)) {
+            psError(PS_ERR_UNKNOWN, false,
+                    "Unable to read concepts from camera and defaults for cell type %s", cellType);
+            return false;
+        }
+    }
+
+    return true;
+}
+
+#if 0
+// Return the level at which EXTENSIONS go, from the FILE metadata within the camera format
+static pmFPALevel hduLevel(const psMetadata *format // The camera format configuration
+                          )
+{
+    assert(format);
+
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *file = psMetadataLookupMetadata(&mdok, format, "FILE"); // File information
+    if (!mdok || !file) {
+        psError(PS_ERR_IO, true, "Unable to find FILE information in camera format configuration.\n");
+        return PM_FPA_LEVEL_NONE;
+    }
+    const char *extType = psMetadataLookupStr(&mdok, file, "EXTENSIONS");
+    if (!mdok || !extType || strlen(extType) == 0) {
+        psError(PS_ERR_IO, true, "Unable to find EXTENSIONS in the FILE information in the camera format"
+                " configuration.\n");
+        return PM_FPA_LEVEL_NONE;
+    }
+
+    // Where do we stick in the HDUs?
+    pmFPALevel level = PM_FPA_LEVEL_NONE; // Level for HDU insertion
+    if (strcasecmp(extType, "CHIP") == 0) {
+        level = PM_FPA_LEVEL_CHIP;
+    } else if (strcasecmp(extType, "CELL") == 0) {
+        level = PM_FPA_LEVEL_CELL;
+    } else if (strcasecmp(extType, "NONE") != 0) {
+        psError(PS_ERR_IO, true, "EXTENSIONS is not CHIP or CELL or NONE.\n");
+    }
+
+    return level;
+}
+#endif
+
+// Find the chip of interest within the FPA
+static bool whichChip(int *chipNum, // Chip number, modified
+                      psString *chipType, // Type of chip, modified
+                      const pmFPA *fpa, // FPA holding chip of interest
+                      const char *content // Content consisting of chipName:chipType
+                      )
+{
+    assert(chipType);
+    assert(fpa);
+    assert(content);
+
+    psArray *chipNames = NULL;
+    psArray *chipTypes = NULL;
+    if (parseContent(&chipNames, &chipTypes, NULL, content) != 1) {
+        psError(PS_ERR_UNKNOWN, false,
+                "Unable to parse chipName:chipType in %s in camera format",
+                TABLE_OF_CONTENTS);
+        return false;
+    }
+
+    psString chipName = psMemIncrRefCounter(chipNames->data[0]); // Name of chip
+    *chipType = psMemIncrRefCounter(chipTypes->data[0]); // Type of chip
+    psFree(chipNames);
+    psFree(chipTypes);
+
+    psTrace("psModules.camera", 5, "This is chip %s\n", chipName);
+
+    // Get the chip
+    *chipNum = pmFPAFindChip(fpa, chipName); // Chip number
+    if (*chipNum == -1) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find chip %s in FPA.\n", chipName);
+        psFree(chipName);
+        return false;
+    }
+    psFree(chipName);
+
+    return true;
+}
+
+
+// Process a chip, using the cellName:cellType pair
+static bool processChip(const psMetadata *format, // Camera format
+                        const char *chipContents, // Contents of chip, cellName:cellType pairs
+                        pmFPA *fpa, // FPA of interest
+                        pmChip *chip, // Chip of interest
+                        pmFPALevel level, // Level for HDU to go
+                        pmHDU *hdu      // HDU to add
+                        )
+{
+    assert(format);
+    assert(chipContents);
+    assert(fpa);
+
+    psMetadata *chips = psMetadataLookupMetadata(NULL, format, CHIP_TYPES); // The chip types
+    if (!chips) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", CHIP_TYPES);
+        return false;
+    }
+
+    psArray *cellNames = NULL;      // Cell names
+    psArray *cellTypes = NULL;      // Cell types
+    if (parseContent(&cellNames, &cellTypes, NULL, chipContents) == 0) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, false,
+                "Unable to parse chip contents (within %s in camera format) as cellName:cellType",
+                CHIP_TYPES);
+        psFree(cellNames);
+        psFree(cellTypes);
+        return false;
+    }
+
+    if (!processContents(fpa, chip, hdu, level, NULL, cellNames, cellTypes, format)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to set contents for chip from camera format.");
+        psFree(cellNames);
+        psFree(cellTypes);
+        return false;
+    }
+
+    psFree(cellNames);
+    psFree(cellTypes);
+
+    return true;
+}
+
+// Given a chip, find the corresponding type by searching through the contents, looking for a match to its
+// name
+psString findChipType(const pmChip *chip, // Chip of interest
+                      psMetadata *contents // Contents, from camera format
+                      )
+{
+    assert(chip);
+    assert(contents);
+
+    const char *chipName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME"); // Name of chip
+    assert(chipName);
+
+    psString chipType = NULL;           // Type of chip
+    psMetadataIterator *iter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item;           // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        if (item->type != PS_DATA_STRING) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                    "Item %s within %s in camera format is not of type STR.", item->name, TABLE_OF_CONTENTS);
+            psFree(iter);
+            psFree(chipType);
+            return NULL;
+        }
+
+        psArray *chipNames = NULL;  // Chip names
+        psArray *chipTypes = NULL;  // Chip types
+        if (parseContent(&chipNames, &chipTypes, NULL, item->data.str) != 1) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, false,
+                    "Unable to parse contents (within %s in camera format) as chipName:chipType",
+                    TABLE_OF_CONTENTS);
+            psFree(chipNames);
+            psFree(chipTypes);
+            psFree(iter);
+            psFree(chipType);
+            return NULL;
+        }
+
+        if (strcmp(chipName, chipNames->data[0]) == 0) {
+            if (chipType) {
+                if (strcmp(chipType, chipTypes->data[0]) != 0) {
+                    psError(PS_ERR_UNKNOWN, true,
+                            "Multiple instances of chip %s in contents, with differing chipType "
+                            "(%s vs %s)", chipName, chipType, (char*)chipTypes->data[0]);
+                    psFree(chipNames);
+                    psFree(chipTypes);
+                    psFree(iter);
+                    psFree(chipType);
+                    return NULL;
+                }
+            } else {
+                chipType = psMemIncrRefCounter(chipTypes->data[0]);
+            }
+        }
+        psFree(chipNames);
+        psFree(chipTypes);
+    }
+    psFree(iter);
+
+    if (!chipType) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to identify chip type for chip %s", chipName);
+        return NULL;
+    }
+
+    return chipType;
+}
+
+// PHU=FPA and EXTENSIONS=CHIP:
+// TABLE_OF_CONTENTS(METADATA) has a list of extensions, each with a chipName:chipType.
+// CHIP_TYPES(METADATA) has a list of chip types, each with cellName:cellType
+static bool addSource_FPA_CHIP(pmFPA *fpa, // FPA to which to add
+                               const psMetadata *format // The camera format
+                               )
+{
+    assert(fpa);
+    assert(format);
+
+    psMetadata *contents = psMetadataLookupMetadata(NULL, format, TABLE_OF_CONTENTS); // The contents
+    if (!contents) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
+        return false;
+    }
+
+    psMetadata *chips = psMetadataLookupMetadata(NULL, format, CHIP_TYPES); // The chip types
+    if (!chips) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", CHIP_TYPES);
+        return false;
+    }
+
+    // Iterate over all extensions
+    psMetadataIterator *contentsIter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(contentsIter))) {
+        if (item->type != PS_DATA_STRING) {
+            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
+                    "Type for %s (%x) in %s METADATA in camera format is not STR",
+                    item->name, item->type, TABLE_OF_CONTENTS);
+            psFree(contentsIter);
+            return false;
+        }
+
+        const char *extname = item->name; // Extension name
+        pmHDU *hdu = pmHDUAlloc(extname); // HDU for this extension
+        // Casting to avoid "warning: passing arg 1 of `p_psMemIncrRefCounter' discards qualifiers from
+        // pointer target type"
+        hdu->format = psMemIncrRefCounter((const psPtr)format);
+
+        // What's in the extension?  It's specified by chipName:chipType
+        // Assume that an extension contains only a single chip, instead of multiple chips
+        psString chipType = NULL;       // Type of chip
+        int chipNum = -1;               // Chip number
+        if (!whichChip(&chipNum, &chipType, fpa, item->data.str)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to determine chip from contents");
+            return false;
+        }
+        pmChip *chip = fpa->chips->data[chipNum]; // Chip of interest
+
+        const char *chipContents = psMetadataLookupStr(NULL, chips, chipType); // Contents of chip
+        psFree(chipType);
+        if (!chipContents) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find chip type %s in %s.",
+                    chipType, CHIP_TYPES);
+            psFree(hdu);
+            psFree(contentsIter);
+            return false;
+        }
+
+        if (!processChip(format, chipContents, fpa, chip, PM_FPA_LEVEL_CHIP, hdu)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to process chip %d\n", chipNum);
+            psFree(hdu);
+            psFree(contentsIter);
+            return false;
+        }
+
+        psFree(hdu);                    // Drop reference
+    }
+    psFree(contentsIter);
+
+    return true;
+}
+
+// PHU=FPA and EXTENSIONS=CELL:
+// TABLE_OF_CONTENTS(METADATA) has a list of extensions, each with a chipName:cellName:cellType.
+static bool addSource_FPA_CELL(pmFPA *fpa, // FPA to which to add
+                               const psMetadata *format // The camera format
+                               )
+{
+    assert(fpa);
+    assert(format);
+
+    psMetadata *contents = psMetadataLookupMetadata(NULL, format, TABLE_OF_CONTENTS); // The contents
+    if (!contents) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
+        return false;
+    }
+
+    // Iterate over all extensions
+    psMetadataIterator *contentsIter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(contentsIter))) {
+        if (item->type != PS_DATA_STRING) {
+            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
+                    "Type for %s (%x) in %s METADATA in camera format is not STR",
+                    item->name, item->type, TABLE_OF_CONTENTS);
+            psFree(contentsIter);
+            return false;
+        }
+
+        const char *extname = item->name; // Extension name
+        pmHDU *hdu = pmHDUAlloc(extname); // HDU for this extension
+        // Casting to avoid "warning: passing arg 1 of `p_psMemIncrRefCounter' discards qualifiers from
+        // pointer target type"
+        hdu->format = psMemIncrRefCounter((const psPtr)format);
+
+        // What's in the extension?  It's specified by (possibly multiple) chipName:cellName:cellType
+
+        psArray *chipNames = NULL;      // Chip names
+        psArray *cellNames = NULL;      // Cell names
+        psArray *cellTypes = NULL;      // Cell types
+        if (parseContent(&chipNames, &cellNames, &cellTypes, item->data.str) == 0) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, false,
+                    "Unable to parse extension contents (within %s->%s in camera format) as "
+                    "chipName:cellName:cellType", TABLE_OF_CONTENTS, extname);
+            psFree(chipNames);
+            psFree(cellNames);
+            psFree(cellTypes);
+            psFree(hdu);
+            psFree(contentsIter);
+            return false;
+        }
+
+        if (!processContents(fpa, NULL, hdu, PM_FPA_LEVEL_CELL, chipNames, cellNames, cellTypes,
+                             format)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to set contents from camera format.");
+            psFree(chipNames);
+            psFree(cellNames);
+            psFree(cellTypes);
+            psFree(hdu);
+            psFree(contentsIter);
+            return false;
+        }
+
+        psFree(chipNames);
+        psFree(cellNames);
+        psFree(cellTypes);
+
+        psFree(hdu);                    // Drop reference
+    }
+    psFree(contentsIter);
+
+    return true;
+}
+
+// PHU=FPA and EXTENSIONS=NONE:
+// TABLE_OF_CONTENTS(STR) has a list of chipName:cellName:cellType.
+static bool addSource_FPA_NONE(pmFPA *fpa, // FPA to which to add
+                               const psMetadata *format // The camera format
+                               )
+{
+    assert(fpa);
+    assert(format);
+
+    psString contents = psMetadataLookupStr(NULL, format, TABLE_OF_CONTENTS); // The contents
+    if (!contents) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
+        return false;
+    }
+
+    // What's in the file?  It's specified by (possibly multiple) chipName:cellName:cellType
+
+    psArray *chipNames = NULL;          // Chip names
+    psArray *cellNames = NULL;          // Cell names
+    psArray *cellTypes = NULL;          // Cell types
+    if (parseContent(&chipNames, &cellNames, &cellTypes, contents) == 0) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, false,
+                "Unable to parse contents (within %s in camera format) as chipName:cellName:cellType",
+                TABLE_OF_CONTENTS);
+        psFree(chipNames);
+        psFree(cellNames);
+        psFree(cellTypes);
+        return false;
+    }
+
+    if (!processContents(fpa, NULL, NULL, PM_FPA_LEVEL_NONE, chipNames, cellNames, cellTypes,
+                         format)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to set contents from camera format.");
+        psFree(chipNames);
+        psFree(cellNames);
+        psFree(cellTypes);
+        return false;
+    }
+
+    psFree(chipNames);
+    psFree(cellNames);
+    psFree(cellTypes);
+
+    return true;
+}
+
+
+// PHU=CHIP and EXTENSIONS=CELL:
+// TABLE_OF_CONTENTS(METADATA) has a menu of contents, each with a chipName:chipType.
+// CHIP_TYPES(METADATA) has a list of chip types(METADATA), each with extension(STR) with cellName:cellType
+static bool addSource_CHIP_CELL(pmFPAview *view, // View for PHU, modified
+                                pmFPA *fpa, // FPA to which to add
+                                pmChip *chip, // Known chip to which to add, or NULL
+                                const psMetadata *format, // The camera format
+                                pmHDU *phdu, // The Primary HDU
+                                bool install // Install the HDUs?
+                                )
+{
+    assert(view);
+    assert(fpa);
+    assert(format);
+    assert(phdu);
+
+    psMetadata *contents = psMetadataLookupMetadata(NULL, format, TABLE_OF_CONTENTS); // The contents
+    if (!contents) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
+        return false;
+    }
+
+    psMetadata *chips = psMetadataLookupMetadata(NULL, format, CHIP_TYPES); // The chip types
+    if (!chips) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", CHIP_TYPES);
+        return false;
+    }
+
+    psMetadata *fileInfo = psMetadataLookupMetadata(NULL, format, "FILE"); // The file information
+    if (!fileInfo) {
+        psError(PS_ERR_IO, false, "Unable to find FILE in the camera format configuration.\n");
+        return false;
+    }
+
+
+    psString chipType = NULL;           // Type of chip
+    if (chip) {
+        // We're given the chip (adding source from view)
+        // Need to identify the chip type, which we will do by traversing the contents
+        chipType = findChipType(chip, contents);
+    } else {
+        // We're given a header, from which to identify what chip we've got, and its type
+        const char *content = getContent(fileInfo, phdu->header, contents); // The contents of this chip
+
+        int chipNum = -1;               // Chip number
+        if (!whichChip(&chipNum, &chipType, fpa, content)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to determine chip from contents");
+            return false;
+        }
+        chip = fpa->chips->data[chipNum]; // Chip of interest
+        view->chip = chipNum;
+    }
+
+    if (!install) {
+        // Everything below is about installing the HDUs
+        psFree(chipType);
+        return true;
+    }
+
+    if (!addHDUtoChip(chip, phdu)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to add HDU to chip\n");
+        psFree(chipType);
+        return false;
+    }
+
+    psMetadata *chipContents = psMetadataLookupMetadata(NULL, chips, chipType); // Contents of chip
+    psFree(chipType);
+    if (!chipContents) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find chip type %s in %s.",
+                chipType, CHIP_TYPES);
+        return false;
+    }
+
+    psMetadataIterator *contentsIter = psMetadataIteratorAlloc(chipContents, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *contentItem;        // Content, from iteration
+    while ((contentItem = psMetadataGetAndIncrement(contentsIter))) {
+        if (contentItem->type != PS_DATA_STRING) {
+            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
+                    "Type for %s (%x) in %s METADATA in camera format is not STR",
+                    contentItem->name, contentItem->type, TABLE_OF_CONTENTS);
+            psFree(contentsIter);
+            return false;
+        }
+
+        const char *extname = contentItem->name; // Extension name
+        pmHDU *hdu = pmHDUAlloc(extname); // HDU for this extension
+        // Casting to avoid "warning: passing arg 1 of `p_psMemIncrRefCounter' discards qualifiers from
+        // pointer target type"
+        hdu->format = psMemIncrRefCounter((const psPtr)format);
+
+        if (!processChip(format, contentItem->data.str, fpa, chip, PM_FPA_LEVEL_CELL, hdu)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to process chip\n");
+            psFree(hdu);
+            psFree(contentsIter);
+            return false;
+        }
+
+        psFree(hdu);                    // Drop reference
+    }
+    psFree(contentsIter);
+
+    if (!pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU, true, true, NULL)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts for chip.");
+        return false;
+    }
+
+    return true;
+}
+
+// PHU=CHIP and EXTENSIONS=NONE:
+// TABLE_OF_CONTENTS(METADATA) has a menu of contents, each with a chipName:chipType.
+// CHIP_TYPES(METADATA) has a list of chip types, each with cellName:cellType
+static bool addSource_CHIP_NONE(pmFPAview *view, // View for PHU, modified
+                                pmFPA *fpa, // FPA to which to add
+                                pmChip *chip, // Known chip to which to add, or NULL
+                                const psMetadata *format, // The camera format
+                                pmHDU *phdu, // Primary HDU
+                                bool install // Install the HDUs?
+                                )
+{
+    assert(fpa);
+    assert(format);
+    assert(phdu);
+
+    psMetadata *contents = psMetadataLookupMetadata(NULL, format, TABLE_OF_CONTENTS); // The contents
+    if (!contents) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
+        return false;
+    }
+
+    psMetadata *chips = psMetadataLookupMetadata(NULL, format, CHIP_TYPES); // The chip types
+    if (!chips) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", CHIP_TYPES);
+        return false;
+    }
+
+    psMetadata *fileInfo = psMetadataLookupMetadata(NULL, format, "FILE"); // The file information
+    if (!fileInfo) {
+        psError(PS_ERR_IO, false, "Unable to find FILE in the camera format configuration.\n");
+        return false;
+    }
+
+    psString chipType = NULL;           // Type of chip
+    if (chip) {
+        // We're given the chip (adding source from view)
+        // Need to identify the chip type, which we will do by traversing the contents
+        chipType = findChipType(chip, contents);
+    } else {
+        const char *content = getContent(fileInfo, phdu->header, contents); // The chip type
+
+        int chipNum = -1;               // Chip number
+        if (!whichChip(&chipNum, &chipType, fpa, content)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to determine chip from contents");
+            return false;
+        }
+        chip = fpa->chips->data[chipNum]; // Chip of interest
+        view->chip = chipNum;
+    }
+
+    if (!install) {
+        // Everything below is about installing the HDU
+        psFree(chipType);
+        return true;
+    }
+
+    if (!addHDUtoChip(chip, phdu)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to add HDU to chip\n");
+        psFree(chipType);
+        return false;
+    }
+
+    // What's in the chip?
+    psString chipContents = psMetadataLookupStr(NULL, chips, chipType); // Contents of the chip
+    if (!chipContents) {
+        psError(PS_ERR_UNEXPECTED_NULL, false,
+                "Unable to find chip type %s in %s of camera format", chipType, CHIP_TYPES);
+        return false;
+    }
+    psFree(chipType);
+
+    if (!processChip(format, chipContents, fpa, chip, PM_FPA_LEVEL_NONE, NULL)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to process chip\n");
+        return false;
+    }
+
+    if (!pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU, true, true, NULL)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts for chip.");
+        return false;
+    }
+
+    return true;
+}
+
+// PHU=CELL and EXTENSIONS=NONE:
+// TABLE_OF_CONTENTS(METADATA) has a menu of contents, each with a chipName:cellName:cellType
+static bool addSource_CELL_NONE(pmFPAview *view, // View for PHU, modified
+                                pmFPA *fpa, // FPA to which to add
+                                pmCell *cell, // Known cell to which to add, or NULL
+                                const psMetadata *format, // The camera format
+                                pmHDU *phdu, // The Primary HDU
+                                bool install // Install the HDUs?
+                                )
+{
+    assert(fpa);
+    assert(format);
+    assert(phdu);
+
+    psMetadata *contents = psMetadataLookupMetadata(NULL, format, TABLE_OF_CONTENTS); // The contents
+    if (!contents) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find %s in camera format.", TABLE_OF_CONTENTS);
+        return false;
+    }
+
+    psMetadata *fileInfo = psMetadataLookupMetadata(NULL, format, "FILE"); // The file information
+    if (!fileInfo) {
+        psError(PS_ERR_IO, false, "Unable to find FILE in the camera format configuration.\n");
+        return false;
+    }
+
+    psArray *chipNames = NULL;          // Chip names
+    psArray *cellNames = NULL;          // Cell names
+    psArray *cellTypes = NULL;          // Cell types
+    pmChip *chip = NULL;                // Chip of interest
+    if (cell) {
+        // We're given the chip and cell (adding source from view)
+        // Need to identify the cell type, which we will do by traversing the contents
+
+        chip = cell->parent;            // The chip of interest
+        psString cellType = NULL;       // Type of cell
+
+        // The below is very similar to findChipType(), but with modifications for finding the cellType
+        const char *chipName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME"); // Name of chip
+        assert(chipName);
+        const char *cellName = psMetadataLookupStr(NULL, cell->concepts, "CELL.NAME"); // Name of cell
+        assert(cellName);
+
+        psMetadataIterator *iter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL); // Iterator
+        psMetadataItem *item;           // Item from iteration
+        while ((item = psMetadataGetAndIncrement(iter))) {
+            if (item->type != PS_DATA_STRING) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                        "Item %s within %s in camera format is not of type STR.",
+                        item->name, TABLE_OF_CONTENTS);
+                psFree(chipNames);
+                psFree(cellNames);
+                psFree(cellTypes);
+                psFree(iter);
+                psFree(cellType);
+                return false;
+            }
+
+            psArray *testChipNames = NULL; // Chip names
+            psArray *testCellNames = NULL; // Cell names
+            psArray *testCellTypes = NULL; // Cell types
+            if (parseContent(&testChipNames, &testCellTypes, &testCellTypes, item->data.str) != 1) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, false,
+                        "Unable to parse contents (within %s in camera format) as chipName:cellName:cellType",
+                        TABLE_OF_CONTENTS);
+                psFree(chipNames);
+                psFree(cellNames);
+                psFree(cellTypes);
+                psFree(testChipNames);
+                psFree(testCellNames);
+                psFree(testCellTypes);
+                psFree(iter);
+                psFree(cellType);
+                return false;
+            }
+
+            if (strcmp(chipName, chipNames->data[0]) == 0 && strcmp(cellName, cellNames->data[0]) == 0) {
+                if (cellType) {
+                    if (strcmp(cellType, cellTypes->data[0]) != 0) {
+                        psError(PS_ERR_UNKNOWN, true,
+                                "Multiple instances of chip %s cell %s in contents, with differing cellType "
+                                "(%s vs %s)", chipName, cellName, cellType, (char*)cellTypes->data[0]);
+                        psFree(chipNames);
+                        psFree(cellNames);
+                        psFree(cellTypes);
+                        psFree(testChipNames);
+                        psFree(testCellNames);
+                        psFree(testCellTypes);
+                        psFree(iter);
+                        psFree(cellType);
+                        return false;
+                    }
+                } else {
+                    cellType = psMemIncrRefCounter(cellTypes->data[0]);
+                    chipNames = psMemIncrRefCounter(testChipNames);
+                    cellNames = psMemIncrRefCounter(testCellNames);
+                    cellTypes = psMemIncrRefCounter(testCellTypes);
+                }
+            }
+            psFree(testChipNames);
+            psFree(testCellNames);
+            psFree(testCellTypes);
+        }
+        psFree(iter);
+
+        if (!cellType) {
+            psError(PS_ERR_UNKNOWN, true, "Unable to identify cell type for chip %s cell %s",
+                    chipName, cellName);
+            psFree(chipNames);
+            psFree(cellNames);
+            psFree(cellTypes);
+            return false;
+        }
+
+        // We don't really care about the cell type here --- it's taken care of by processContents
+        psFree(cellType);
+
+    } else {
+        const char *content = getContent(fileInfo, phdu->header, contents); // Content of cell
+
+        if (parseContent(&chipNames, &cellNames, &cellTypes, content) != 1) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, false,
+                    "Unable to parse cell contents (%s) as cellName:cellType", content);
+            psFree(chipNames);
+            psFree(cellNames);
+            psFree(cellTypes);
+            return false;
+        }
+
+        int chipNum = pmFPAFindChip(fpa, chipNames->data[0]); // Chip number
+        if (chipNum == -1) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to find chip %s referred to in contents",
+                    (char*)chipNames->data[0]);
+            psFree(chipNames);
+            psFree(cellNames);
+            psFree(cellTypes);
+            return false;
+        }
+        chip = fpa->chips->data[chipNum];
+
+        int cellNum = pmChipFindCell(chip, cellNames->data[0]); // Cell number
+        if (cellNum == -1) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to find cell %s referred to in contents",
+                    (char*)cellNames->data[0]);
+            psFree(chipNames);
+            psFree(cellNames);
+            psFree(cellTypes);
+            return false;
+        }
+        cell = chip->cells->data[cellNum];
+
+        view->chip = chipNum;
+        view->cell = cellNum;
+
+        psFree(chipNames);
+        psFree(cellNames);
+        psFree(cellTypes);
+    }
+
+    if (!install) {
+        // Everything below is about installing the HDU
+        psFree(chipNames);
+        psFree(cellNames);
+        psFree(cellTypes);
+        return true;
+    }
+
+    if (!processContents(fpa, NULL, phdu, PM_FPA_LEVEL_NONE, chipNames, cellNames, cellTypes, format)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to set contents for cell from camera format.");
+        psFree(chipNames);
+        psFree(cellNames);
+        psFree(cellTypes);
+        return false;
+    }
+
+    psFree(chipNames);
+    psFree(cellNames);
+    psFree(cellTypes);
+
+    if (!pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU, true, NULL)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts for cell.");
+        return false;
+    }
+
+    return true;
+}
+
+
+// This is the engine for the pmFPAAddSourceFrom{Header,View} functions.
+// It uses the camera format configuration information to determine where HDUs go in the FPA.
+// It returns a view corresponding to the PHU
+static pmFPAview *addSource(pmFPA *fpa,       // The FPA
+                            const char *fpaname, // The desired FPA name
+                            const pmFPAview *phuView, // The view corresponding to the PHU, or NULL
+                            const psMetadata *header, // The PHU header, or NULL
+                            const psMetadata *format, // Format of file
+                            bool install // Install the provided header in the location that we find?
+                           )
+{
+    assert(fpa);
+    assert(phuView || header);
+    assert(format);
+
+    bool mdok;                          // Status of MD lookup
+
+    // If FPA.NAME is already defined, new name must match it; otherwise, warn the user that something
+    // potentially bad is happening.
+    if (fpaname && install) {
+        const char *currentName = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.NAME"); // Current name
+        if (mdok && currentName && strlen(currentName) > 0 && strcmp(currentName, fpaname) != 0) {
+            psWarning("FPA.NAME for new source (%s) doesn't match FPA.NAME for current "
+                     "fpa (%s).\n", fpaname, currentName);
+        }
+        psMetadataAddStr(fpa->concepts, PS_LIST_HEAD, "FPA.NAME", PS_META_REPLACE, "Name of FPA", fpaname);
+    } else if (!psMetadataLookup(fpa->concepts, "FPA.NAME")) {
+        // Make sure there is an FPA.NAME
+        psMetadataAddStr(fpa->concepts, PS_LIST_HEAD, "FPA.NAME", 0, "Name of FPA", "UNKNOWN");
+    }
+
+    psMetadata *fileInfo = psMetadataLookupMetadata(&mdok, format, "FILE"); // The file information
+    if (!mdok || !fileInfo) {
+        psError(PS_ERR_IO, false, "Unable to find FILE in the camera format configuration.\n");
+        return NULL;
+    }
+
+    // At what level does the PHU go?
+    const char *phuType = psMetadataLookupStr(&mdok, fileInfo, "PHU"); // What is the PHU?
+    if (!mdok || strlen(phuType) == 0) {
+        psError(PS_ERR_IO, false, "Unable to find PHU in the format specification.\n");
+        return NULL;
+    }
+
+    // Prepare the PHU to be placed in the camera hierarchy
+    pmHDU *phdu = pmHDUAlloc(NULL);     // The primary header data unit
+    // Casting to psPtr to avoide "warning: passing arg 1 of `p_psMemIncrRefCounter' discards qualifiers from
+    // pointer target type"
+    phdu->header = psMemIncrRefCounter((const psPtr)header);
+    phdu->format = psMemIncrRefCounter((const psPtr)format);
+    pmFPAview *view = pmFPAviewAlloc(0); // View, to be returned
+    if (phuView) {
+        // Copy the view, for the case where we're given a view.
+        *view = *phuView;
+    }
+
+    // And at what level do the individual extensions go?
+    const char *extType = psMetadataLookupStr(&mdok, fileInfo, "EXTENSIONS"); // What's in the extns?
+    if (!mdok || strlen(extType) == 0) {
+        psError(PS_ERR_IO, false, "Unable to find EXTENSIONS in the format specification.\n");
+        psFree(view);
+        return NULL;
+    }
+
+    // Now, there are a few cases:
+
+    // Case    PHU     EXTENSIONS     Description
+    // ====    ===     ==========     ===========
+    // 1.      FPA     CHIP           CONTENTS(METADATA) has a list of extensions, each with chipName:chipType
+    //                                CHIPS(METADATA) has a list of chip types, each with cell:type
+    // 2.      FPA     CELL           CONTENTS(METADATA) has a list of extensions, each with chip:cell:type
+    //                                No need for CHIPS.
+    // 3.      FPA     NONE           CONTENTS(STRING) has a list of extensions, chip:cell:type
+    //                                No need for CHIPS
+    // 4.      CHIP    CELL           CONTENTS(METADATA) is a menu, each with a chipName:chipType
+    //                                CHIPS(METADATA) has a list of chip types(METADATA), containg a list of
+    //                                extensions.
+    // 5.      CHIP    NONE           CONTENTS(METADATA) is a menu, each with a chipName:chipType
+    //                                CHIPS(METADATA) has a list of chip types(STRING) with cell:type
+    // 6.      CELL    NONE           CONTENTS(METADATA) is a menu, each with a chipName:cellName:cellType.
+    //                                No need for CHIPS.
+
+
+    pmFPALevel phuLevel = pmFPALevelFromName(phuType); // Level for PHU
+    pmFPALevel extLevel = pmFPALevelFromName(extType); // Level for extensions
+
+    switch (phuLevel) {
+      case PM_FPA_LEVEL_FPA: {
+          // We don't have to work out where the PHU is --- there's only one FPA.
+          // 'view' already points to the FPA.
+          switch (extLevel) {
+            case PM_FPA_LEVEL_CHIP:
+              phdu->blankPHU = true;
+              if (install) {
+                  if (!addHDUtoFPA(fpa, phdu) || !addSource_FPA_CHIP(fpa, format) ||
+                      !pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU,
+                                         true, NULL)) {
+                      psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
+                      psFree(phdu);
+                      psFree(view);
+                      return NULL;
+                  }
+              }
+              psFree(phdu);
+              return view;
+            case PM_FPA_LEVEL_CELL:
+              if (install) {
+                  phdu->blankPHU = true;
+                  if (!addHDUtoFPA(fpa, phdu) || !addSource_FPA_CELL(fpa, format) ||
+                      !pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU,
+                                         true, NULL)) {
+                      psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
+                      psFree(phdu);
+                      psFree(view);
+                      return NULL;
+                  }
+              }
+              psFree(phdu);
+              return view;
+            case PM_FPA_LEVEL_NONE:
+              if (install) {
+                  phdu->blankPHU = false;
+                  if (!addHDUtoFPA(fpa, phdu) || !addSource_FPA_NONE(fpa, format) ||
+                      !pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS | PM_CONCEPT_SOURCE_PHU,
+                                         true, NULL)) {
+                      psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
+                      psFree(phdu);
+                      psFree(view);
+                      return NULL;
+                  }
+              }
+              psFree(phdu);
+             return view;
+            default:
+              psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                      "EXTENSIONS level (%s) incompatible with PHU level (FPA)", extType);
+              psFree(phdu);
+              psFree(view);
+              return NULL;
+          }
+          break;
+      }
+      case PM_FPA_LEVEL_CHIP: {
+          pmChip *chip = NULL;          // Appropriate chip, if the view is specified
+          if (phuView) {
+              chip = fpa->chips->data[phuView->chip];
+          }
+          switch (extLevel) {
+            case PM_FPA_LEVEL_CELL:
+              phdu->blankPHU = true;
+              if (!addSource_CHIP_CELL(view, fpa, chip, format, phdu, install)) {
+                  psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
+                  psFree(phdu);
+                  psFree(view);
+                  return NULL;
+              }
+              psFree(phdu);
+              return view;
+            case PM_FPA_LEVEL_NONE:
+              phdu->blankPHU = false;
+              if (!addSource_CHIP_NONE(view, fpa, chip, format, phdu, install)) {
+                  psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
+                  psFree(phdu);
+                  psFree(view);
+                  return NULL;
+              }
+              psFree(phdu);
+              return view;
+            default:
+              psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                      "EXTENSIONS level (%s) incompatible with PHU level (CHIP)", extType);
+              return NULL;
+          }
+          break;
+      }
+      case PM_FPA_LEVEL_CELL: {
+          if (extLevel != PM_FPA_LEVEL_NONE) {
+              psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                      "EXTENSIONS level (%s) incompatible with PHU level (CELL)", extType);
+              return NULL;
+          }
+          pmChip *chip = NULL;          // Appropriate chip, if the view is specified
+          pmCell *cell = NULL;          // Appropriate cell, if the view is specified
+          if (phuView) {
+              chip = fpa->chips->data[phuView->chip];
+              cell = chip->cells->data[phuView->cell];
+          }
+          phdu->blankPHU = false;
+          if (!addSource_CELL_NONE(view, fpa, cell, format, phdu, install)) {
+              psError(PS_ERR_UNKNOWN, false, "Unable to add source.");
+              psFree(phdu);
+              psFree(view);
+              return NULL;
+          }
+          psFree(phdu);
+          return view;
+          break;
+      }
+      default:
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Bad PHU level: %s", phuType);
+        return NULL;
+    }
+
+    return NULL;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+pmFPA *pmFPAConstruct(const psMetadata *camera)
+{
+    PS_ASSERT_PTR_NON_NULL(camera, NULL);
+
+    pmFPA *fpa = pmFPAAlloc(camera);    // The FPA to fill out
+
+    bool mdok = true;                   // Status from MD lookups
+    psMetadata *components = psMetadataLookupMetadata(&mdok, camera, "FPA"); // FPA components
+    if (!mdok || !components) {
+        psError(PS_ERR_IO, true, "Failed to lookup \"FPA\"");
+        psFree(fpa);
+        return NULL;
+    }
+    psMetadataIterator *componentsIter = psMetadataIteratorAlloc(components, PS_LIST_HEAD, NULL);
+    psMetadataItem *componentsItem = NULL; // Item from components
+    while ((componentsItem = psMetadataGetAndIncrement(componentsIter))) {
+        const char *chipName = componentsItem->name; // Name of the chip
+        if (componentsItem->type != PS_DATA_STRING) {
+            psLogMsg(__func__, PS_LOG_WARN, "Element %s in FPA within the camera configuration is not of "
+                     "type STR (type=%x) --- ignored.\n", chipName, componentsItem->type);
+            continue;
+        }
+
+        pmChip *chip = pmChipAlloc(fpa, chipName); // The chip
+        psList *cellNames = psStringSplit(componentsItem->data.V, " ,;", true); // List of cell names
+        psListIterator *cellNamesIter = psListIteratorAlloc(cellNames, PS_LIST_HEAD, false); // Iterator
+
+        psString cellName = NULL;       // Name of cell
+        while ((cellName = psListGetAndIncrement(cellNamesIter))) {
+            pmCell *cell = pmCellAlloc(chip, cellName); // New cell
+            psFree(cell);               // Drop reference
+        }
+        psFree(chip);                   // Drop reference
+        psFree(cellNamesIter);
+        psFree(cellNames);
+    }
+    psFree(componentsIter);
+
+    return fpa;
+}
+
+bool pmFPAAddSourceFromView(pmFPA *fpa, const char *fpaname, const pmFPAview *phuView,
+                            const psMetadata *format)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(phuView, false);
+    PS_ASSERT_PTR_NON_NULL(format, false);
+
+    pmFPAview *view = addSource(fpa, fpaname, phuView, NULL, format, true);
+    bool status = (view != NULL);
+    psFree(view);
+    return status;
+}
+
+pmFPAview *pmFPAAddSourceFromHeader(pmFPA *fpa, psMetadata *phu, const psMetadata *format)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    PS_ASSERT_PTR_NON_NULL(phu, NULL);
+    PS_ASSERT_PTR_NON_NULL(format, NULL);
+
+    bool mdok = true;                   // Status from metadata lookups
+    psMetadata *fileInfo = psMetadataLookupMetadata(&mdok, format, "FILE"); // The file information
+    if (!mdok || !fileInfo) {
+        psError(PS_ERR_IO, false, "Unable to find FILE in the camera format configuration.\n");
+        return NULL;
+    }
+
+    // Check the name of the FPA
+    psString fpaname = phuNameFromHeader("FPA.NAME", fileInfo, phu); // New name for the FPA
+    if (!fpaname || strlen(fpaname) == 0) {
+        psWarning("Unable to determine FPA.NAME: check for FPA.NAME in FILE in camera format");
+    }
+
+    pmFPAview *view = addSource(fpa, fpaname, NULL, phu, format, true); // View of PHU, to return
+    psFree(fpaname);
+
+    return view;
+}
+
+
+pmFPAview *pmFPAIdentifySourceFromHeader(pmFPA *fpa, psMetadata *phu, const psMetadata *format)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    PS_ASSERT_PTR_NON_NULL(phu, NULL);
+    PS_ASSERT_PTR_NON_NULL(format, NULL);
+
+    return addSource(fpa, NULL, NULL, phu, format, false);
+}
+
+
+// Print spaces to indent
+#define INDENT(FILE, LEVEL) \
+{ \
+    for (int i = 0; i < (LEVEL); i++) { \
+        fprintf(FILE, " "); \
+    } \
+}
+
+void pmFPAPrint(FILE *fd, const pmFPA *fpa, bool header, bool concepts)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa,);
+
+    INDENT(fd, 1);
+    fprintf(fd, "FPA:\n");
+    if (fpa->hdu) {
+        pmHDUPrint(fd, fpa->hdu, 2, header);
+    }
+    if (concepts) {
+        psMetadataPrint(fd, fpa->concepts, 2);
+    }
+
+    psArray *chips = fpa->chips;        // Array of chips
+    // Iterate over the FPA
+    for (int i = 0; i < chips->n; i++) {
+        INDENT(fd, 3);
+        fprintf(fd, "Chip: %d\n", i);
+        pmChip *chip = chips->data[i]; // The chip
+        if (chip->hdu) {
+            pmHDUPrint(fd, chip->hdu, 4, header);
+        }
+        if (concepts) {
+            psMetadataPrint(fd, chip->concepts, 4);
+        }
+
+        // Iterate over the chip
+        psArray *cells = chip->cells;   // Array of cells
+        for (int j = 0; j < cells->n; j++) {
+            INDENT(fd, 5);
+            fprintf(fd, "Cell: %d\n", j);
+            pmCell *cell = cells->data[j]; // The cell
+            if (cell->hdu) {
+                pmHDUPrint(fd, cell->hdu, 6, header);
+            }
+            if (concepts) {
+                psMetadataPrint(fd, cell->concepts, 6);
+            }
+
+            psArray *readouts = cell->readouts; // Array of readouts
+            for (int k = 0; k < readouts->n; k++) {
+                pmReadout *readout = readouts->data[k]; // The readout
+                INDENT(fd, 6);
+                fprintf(fd, "Readout %d:\n", k);
+                INDENT(fd, 7);
+                fprintf(fd, "col0: %d\n", readout->col0);
+                INDENT(fd, 7);
+                fprintf(fd, "row0: %d\n", readout->row0);
+                psImage *image = readout->image; // The image
+                psImage *mask = readout->mask; // The mask
+                psImage *weight = readout->weight; // The weight
+                psList *bias = readout->bias; // The list of bias images
+                if (image) {
+                    INDENT(fd, 7);
+                    fprintf(fd, "Image: [%d:%d,%d:%d] (%dx%d)\n", image->col0, image->col0 +
+                            image->numCols, image->row0, image->row0 + image->numRows, image->numCols,
+                            image->numRows);
+                }
+                if (bias) {
+                    psListIterator *biasIter = psListIteratorAlloc(bias, PS_LIST_HEAD, false); // Iterator
+                    psImage *biasImage = NULL; // Bias image from iteration
+                    while ((biasImage = psListGetAndIncrement(biasIter))) {
+                        INDENT(fd, 7);
+                        fprintf(fd, "Bias:  [%d:%d,%d:%d] (%dx%d)\n", biasImage->col0,
+                                biasImage->col0 + biasImage->numCols, biasImage->row0,
+                                biasImage->row0 + biasImage->numRows, biasImage->numCols, biasImage->numRows);
+                    }
+                    psFree(biasIter);
+                }
+                if (mask) {
+                    INDENT(fd, 7);
+                    fprintf(fd, "Mask: [%d:%d,%d:%d] (%dx%d)\n", mask->col0, mask->col0 +
+                            mask->numCols, mask->row0, mask->row0 + mask->numRows, mask->numCols,
+                            mask->numRows);
+                }
+                if (weight) {
+                    INDENT(fd, 7);
+                    fprintf(fd, "Weight: [%d:%d,%d:%d] (%dx%d)\n", weight->col0, weight->col0 +
+                            weight->numCols, weight->row0, weight->row0 + weight->numRows, weight->numCols,
+                            weight->numRows);
+                }
+            } // Iterating over cell
+        } // Iterating over chip
+    } // Iterating over FPA
+
+}
+
+
+pmFPALevel pmFPAPHULevel(const psMetadata *format)
+{
+    PS_ASSERT_METADATA_NON_NULL(format, PM_FPA_LEVEL_NONE);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *fileInfo = psMetadataLookupMetadata(&mdok, format, "FILE"); // Contents of FILE metadata
+    if (!mdok || !fileInfo) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find FILE in camera format configuration.\n");
+        return PM_FPA_LEVEL_NONE;
+    }
+    const char *phu = psMetadataLookupStr(&mdok, fileInfo, "PHU"); // PHU level
+    if (!mdok || !phu || strlen(phu) == 0) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find PHU in FILE in camera format configuration.\n");
+        return PM_FPA_LEVEL_NONE;
+    }
+
+    return pmFPALevelFromName(phu);
+}
+
+pmFPALevel pmFPAExtensionsLevel(const psMetadata *format)
+{
+    PS_ASSERT_METADATA_NON_NULL(format, PM_FPA_LEVEL_NONE);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *fileInfo = psMetadataLookupMetadata(&mdok, format, "FILE"); // Contents of FILE metadata
+    if (!mdok || !fileInfo) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find FILE in camera format configuration.\n");
+        return PM_FPA_LEVEL_NONE;
+    }
+
+    const char *extensions = psMetadataLookupStr(&mdok, fileInfo, "EXTENSIONS"); // EXTENSIONS level
+    if (!mdok || !extensions || strlen(extensions) == 0) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find EXTENSIONS in FILE in camera format configuration.\n");
+        return PM_FPA_LEVEL_NONE;
+    }
+
+    return pmFPALevelFromName(extensions);
+}
+
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAConstruct.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAConstruct.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAConstruct.h	(revision 15358)
@@ -0,0 +1,69 @@
+/* @file pmFPAConstruct.h
+ * @brief Functions to create an FPA, and add data sources to it.
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_CONSTRUCT_H
+#define PM_FPA_CONSTRUCT_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Construct an FPA instance on the basis of a camera configuration
+///
+/// This is the function that creates the FPA hierarchy on the basis of the camera configuration.  The "FPA"
+/// entry in the camera configuration specifies the chips (each of type STR) with their component cells listed
+/// as the corresponding values (whitespace separated).  The FPA hierarchy is created devoid of any
+/// input/output sources (i.e., HDUs).
+pmFPA *pmFPAConstruct(const psMetadata *camera ///< The camera configuration
+                     );
+
+/// Add an (input or output) source to the focal plane hierarchy, specified by a view
+///
+/// Given an FPA, add an HDU by specifying where it goes (i.e., by an FPAview).  The camera format
+/// configuration is required in order to describe how the FPA is laid out in terms of disk files.
+bool pmFPAAddSourceFromView(pmFPA *fpa,   ///< The FPA
+                            const char *fpaname, ///< FPA.NAME for the source
+                            const pmFPAview *phuView, ///< The view, corresponding to the PHU
+                            const psMetadata *format ///< Format of file
+                           );
+
+/// Add an (input or output) source to the focal plane hierarchy, specified by a (primary) header
+///
+/// Given an FPA, add an HDU by specifying a primary header, which is used to determine the FITS file
+/// contents, and therefore the proper location for the HDU.  The camera format configuration is required in
+/// order to describe how the FPA is laid out in terms of disk files.
+pmFPAview *pmFPAAddSourceFromHeader(pmFPA *fpa, ///< The FPA
+                                    psMetadata *phu, ///< Primary header of file
+                                    const psMetadata *format ///< Format of file
+                                   );
+
+/// Identify a source in the focal plane hierarchy, specified by a (primary) header
+///
+/// This is the same as pmFPAAddSourceFromHeader, except the input is not added into the FPA hierarchy.
+/// This function serves only to identify where in the hierarchy it should go, not prepare for reading, etc.
+pmFPAview *pmFPAIdentifySourceFromHeader(pmFPA *fpa, psMetadata *phu, const psMetadata *format);
+
+/// Print a representation of the FPA, including its headers and concepts.
+///
+/// This function is intended for testing and development purposes.
+void pmFPAPrint(FILE *fd,               ///< File descriptor to which to print
+                const pmFPA *fpa,       ///< FPA to print
+                bool header,            ///< Print headers?
+                bool concepts           ///< Print concepts?
+               );
+
+/// Return the PHU level for an FPA, given the format
+pmFPALevel pmFPAPHULevel(const psMetadata *format);
+
+/// Return the Extensions level for an FPA, given the format
+pmFPALevel pmFPAExtensionsLevel(const psMetadata *format);
+
+/// @}
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPACopy.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPACopy.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPACopy.c	(revision 15358)
@@ -0,0 +1,546 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <strings.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAUtils.h"
+#include "pmHDUUtils.h"
+#include "pmFPACopy.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions and macros
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Copy the value for a concept
+#define COPY_CONCEPT(TARGET, SOURCE, NAME, TYPE) { \
+    psMetadataItem *targetItem = psMetadataLookup(TARGET, NAME); \
+    psMetadataItem *sourceItem = psMetadataLookup(SOURCE, NAME); \
+    targetItem->data.TYPE = sourceItem->data.TYPE; }
+
+// Find the blank (image-less) PHU, given a cell.
+static pmHDU *findBlankPHU(const pmCell *cell // The cell for which to find the PHU
+                          )
+{
+    assert(cell);
+
+    if (cell->hdu && cell->hdu->blankPHU) {
+        return cell->hdu;
+    }
+    pmChip *chip = cell->parent;        // The parent chip
+    if (chip->hdu && chip->hdu->blankPHU) {
+        return chip->hdu;
+    }
+    pmFPA *fpa = chip->parent;  // The parent FPA
+    if (fpa->hdu && fpa->hdu->blankPHU) {
+        return fpa->hdu;
+    }
+
+    return NULL;
+}
+
+// copy one of the psImage components of the readout
+static void readoutCopyComponent (psImage **target, psImage *source, psImageBinning *binning, bool xFlip, bool yFlip, bool pixels) {
+
+    if (!source) return;
+
+    if (*target) {
+        psFree(*target);
+    }
+    if (pixels) {
+        *target = psImageFlip(NULL, source, xFlip, yFlip);
+        return;
+    }
+
+    // I have the fine image size, I know the binning factor, determine the ruff image size
+    binning->nXfine = source->numCols;
+    binning->nYfine = source->numRows;
+    psImageBinningSetRuffSize(binning, PS_IMAGE_BINNING_CENTER);
+    *target = psImageAlloc(binning->nXruff, binning->nYruff, source->type.type);
+    return;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static engine functions --- these do all the work.  Actually, cellCopy does all the work; the others
+// merely iterate on the higher-level components.
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Common engine for pmCellCopy and pmCellCopyStructure
+// Does the actual splitting/splicing that's required to copy an FPA to a different representation.
+static bool cellCopy(pmCell *target,     // The target cell
+                     const pmCell *source, // The source cell, to be copied
+                     bool pixels,        // Copy the pixels?
+                     int xBin, int yBin  // (Relative) binning factors in x and y
+                    )
+{
+    assert(target);
+    assert(source);
+    assert(xBin > 0 && yBin > 0);
+
+    if (!source->data_exists) {
+        // Copied everything that exists
+        return true;
+    }
+
+    // XXX this is a programming / config error
+    if (pixels && (xBin != 1 || yBin != 1)) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to copy pixels if binning is set\n");
+        return false;
+    }
+
+    // the binning structure carries the information no how to rebin the images if needed
+    psImageBinning *binning = psImageBinningAlloc();
+    binning->nXbin = xBin;
+    binning->nYbin = yBin;
+
+    psArray *sourceReadouts = source->readouts; // The source readouts
+    int numReadouts = sourceReadouts->n; // Number of readouts copied
+
+    // Need to check/change CELL.XPARITY and CELL.YPARITY
+    bool mdokS = true;                   // Status of MD lookup
+    bool mdokT = true;                   // Status of MD lookup
+    bool xFlip = false;                 // Switch parity in x?
+    bool yFlip = false;                 // Switch parity in y?
+
+    // enforce the following conditions:
+    // CELL.XPARITY is required
+    // CELL.XPARITY must be +/- 1
+    int xParitySource = psMetadataLookupS32(&mdokS, source->concepts, "CELL.XPARITY"); // Source parity
+    int xParityTarget = psMetadataLookupS32(&mdokT, target->concepts, "CELL.XPARITY"); // Target x parity
+    assert(mdokS && mdokT);
+    if (abs(xParitySource) != 1 || abs(xParityTarget) != 1) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "CELL.XPARITY is not set for both source (%d) and target (%d)",
+                xParitySource, xParityTarget);
+        psFree(binning);
+        return false;
+    }
+    if (xParityTarget != xParitySource) {
+        xFlip = true;
+    } else {
+        // Use the source parity
+        COPY_CONCEPT(target->concepts, source->concepts, "CELL.XPARITY", S32);
+    }
+
+    int yParityTarget = psMetadataLookupS32(&mdokT, target->concepts, "CELL.YPARITY"); // Target y parity
+    int yParitySource = psMetadataLookupS32(&mdokS, source->concepts, "CELL.YPARITY"); // Source parity
+    assert(mdokS && mdokT);
+    if (abs(yParitySource) != 1 || abs(yParityTarget) != 1) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "CELL.YPARITY is not set for both source (%d) and target (%d)",
+                yParitySource, yParityTarget);
+        psFree(binning);
+        return false;
+    }
+    if (yParityTarget != yParitySource) {
+        yFlip = true;
+    } else {
+        // Use the source parity
+        COPY_CONCEPT(target->concepts, source->concepts, "CELL.YPARITY", S32);
+    }
+    psTrace("psModules.camera", 3, "xFlip: %d; yFlip: %d\n", xFlip, yFlip);
+
+    // Perform deep copy of the images.  I would prefer *not* to do a deep copy, in the interests of speed (we
+    // still need to do another deep copy into the HDU for when we write out), but this is the only way I can
+    // think of to provide security against copying a cell and then unknowingly changing the source when
+    // manipulating the target.
+    for (int i = 0; i < numReadouts; i++) {
+        pmReadout *sourceReadout = sourceReadouts->data[i]; // The source readout
+        pmReadout *targetReadout = pmReadoutAlloc(target); // The target readout; this adds it to the cell
+
+        // Copy attributes
+        // XXX is this correct under binning?
+        targetReadout->col0 = sourceReadout->col0;
+        targetReadout->row0 = sourceReadout->row0;
+        targetReadout->process = sourceReadout->process;
+        targetReadout->file_exists = sourceReadout->file_exists;
+        targetReadout->data_exists = sourceReadout->data_exists;
+
+        // Copy all three image components (image, mask, weight)
+        readoutCopyComponent (&targetReadout->image,  sourceReadout->image,  binning, xFlip, yFlip, pixels);
+        readoutCopyComponent (&targetReadout->mask,   sourceReadout->mask,   binning, xFlip, yFlip, pixels);
+        readoutCopyComponent (&targetReadout->weight, sourceReadout->weight, binning, xFlip, yFlip, pixels);
+
+        // Copy bias
+        while (targetReadout->bias->n > 0) {
+            psListRemove(targetReadout->bias, PS_LIST_HEAD);
+        }
+
+        // Iterate over the biases
+        psListIterator *biasIter = psListIteratorAlloc(sourceReadout->bias, PS_LIST_HEAD, false);
+        psImage *bias = NULL;           // Bias image from iteration
+        while ((bias = psListGetAndIncrement(biasIter))) {
+            psImage *biasCopy = NULL;          // Copy of the bias
+            readoutCopyComponent (&biasCopy, bias, binning, xFlip, yFlip, pixels);
+            psListAdd(targetReadout->bias, PS_LIST_TAIL, biasCopy);
+            psFree(biasCopy);           // Drop reference
+        }
+        psFree(biasIter);
+
+        targetReadout->data_exists = true;
+        psFree(targetReadout);          // Drop reference
+    }
+
+    // Copy the remaining "concepts" over.  Don't copy the TRIMSEC or BIASSEC, since these will be created by
+    // pmHDUGenerate if they don't already exist in the target.  Don't copy the XPARITY or YPARITY, since
+    // we've used those to do the flips.  Don't copy the X0 and Y0 because they are updated below (and are
+    // dependent upon the flips we've done above).
+    psMetadataIterator *conceptsIter = psMetadataIteratorAlloc(source->concepts, PS_LIST_HEAD, NULL);
+    psMetadataItem *conceptItem = NULL; // Item from iteration
+    while ((conceptItem = psMetadataGetAndIncrement(conceptsIter))) {
+        psString name = conceptItem->name; // Name of concept
+        if (!strcmp(name, "CELL.TRIMSEC")) continue;
+        if (!strcmp(name, "CELL.BIASSEC")) continue;
+        if (!strcmp(name, "CELL.XPARITY")) continue;
+        if (!strcmp(name, "CELL.YPARITY")) continue;
+        if (!strcmp(name, "CELL.X0")) continue;
+        if (!strcmp(name, "CELL.Y0")) continue;
+
+        psMetadataItem *copy = psMetadataItemCopy(conceptItem); // Copy of the concept
+        psMetadataAddItem(target->concepts, copy, PS_LIST_TAIL, PS_META_REPLACE);
+        psFree(copy);               // Drop reference
+    }
+    psFree(conceptsIter);
+
+    // Need to update CELL.TRIMSEC and CELL.BIASSEC if we changed the binning and they exist already.
+    // XXX this code seems to be very similar to pmConceptsUpdate
+    if ((binning->nXbin != 1) || (binning->nYbin != 1)) {
+        bool mdok = false;
+        psRegion *trimsec = psMetadataLookupPtr(&mdok, target->concepts, "CELL.TRIMSEC"); // The trim section
+        if (mdok && trimsec && !psRegionIsNaN(*trimsec)) {
+            *trimsec = psImageBinningSetRuffRegion(binning, *trimsec);
+            // force integer pixels : truncate x0, roundup x1:
+            trimsec->x0 = (int)trimsec->x0;
+            if (trimsec->x1 > (int)trimsec->x1) {
+                trimsec->x1 = (int)trimsec->x1 + 1;
+            } else {
+                trimsec->x1 = (int)trimsec->x1;
+            }
+            trimsec->y0 = (int)trimsec->y0;
+            if (trimsec->y1 > (int)trimsec->y1) {
+                trimsec->y1 = (int)trimsec->y1 + 1;
+            } else {
+                trimsec->y1 = (int)trimsec->y1;
+            }
+        }
+        psList *biassecs = psMetadataLookupPtr(&mdok, target->concepts, "CELL.BIASSEC"); // The bias sections
+        if (mdok && biassecs && biassecs->n > 0) {
+            psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, true); // Iterator
+            psRegion *biassec = NULL;   // Bias section, from iteration
+            while ((biassec = psListGetAndIncrement(biassecsIter))) {
+                if (!psRegionIsNaN(*biassec)) {
+                    *biassec = psImageBinningSetRuffRegion(binning, *biassec);
+                    // force integer pixels : truncate x0, roundup x1:
+                    biassec->x0 = (int)biassec->x0;
+                    if (biassec->x1 > (int)biassec->x1) {
+                        biassec->x1 = (int)biassec->x1 + 1;
+                    } else {
+                        biassec->x1 = (int)biassec->x1;
+                    }
+                    biassec->y0 = (int)biassec->y0;
+                    if (biassec->y1 > (int)biassec->y1) {
+                        biassec->y1 = (int)biassec->y1 + 1;
+                    } else {
+                        biassec->y1 = (int)biassec->y1;
+                    }
+                }
+            }
+            psFree(biassecsIter);
+        }
+    }
+
+    // Need to update CELL.X0 and CELL.Y0 if we flipped
+    // XXX this section should probably use a common function consistent with psImageBinning
+    if (xFlip) {
+        int xZero = psMetadataLookupS32(NULL, source->concepts, "CELL.X0"); // CELL.X0 from source
+        int xParity = psMetadataLookupS32(NULL, source->concepts, "CELL.XPARITY"); // Parity in x
+        int sourceBin = psMetadataLookupS32(NULL, source->concepts, "CELL.XBIN"); // CELL.XBIN from source
+        int xSize = psMetadataLookupS32(NULL, source->concepts, "CELL.XSIZE"); // CELL.XSIZE of source
+
+        if (sourceBin == 0) {
+            // Don't know the binning; assume it is unity
+            sourceBin = binning->nXbin;
+        }
+
+        // XXX make sure this is consistent with the psImageBinning
+        psTrace("psModules.camera", 3, "CELL.X0: Before: %d After: %d\n", xZero, xZero + (xSize - 1) * xParity * sourceBin);
+        psTrace("psModules.camera", 9, "(xParity: %d xBin: %d numCols: %d)\n", xParity, sourceBin, xSize);
+
+        if (xParity == 0 || xSize == 0) {
+            psWarning("New CELL.X0 may be incorrect due to missing concepts (CELL.XPARITY, CELL.XSIZE)");
+        }
+
+        xZero += (xSize - 1) * xParity * sourceBin; // Change the parity on the X0 position
+        psMetadataItem *newItem = psMetadataLookup(target->concepts, "CELL.X0"); // CELL.X0 from target
+        newItem->data.S32 = xZero;
+    }
+    if (yFlip) {
+        int yZero = psMetadataLookupS32(NULL, source->concepts, "CELL.Y0"); // CELL.Y0 from source
+        int yParity = psMetadataLookupS32(NULL, source->concepts, "CELL.YPARITY"); // Parity in y
+        int sourceBin = psMetadataLookupS32(NULL, source->concepts, "CELL.YBIN"); // Binning in y
+        int ySize = psMetadataLookupS32(NULL, source->concepts, "CELL.YSIZE"); // CELL.YSIZE of source
+
+        if (sourceBin == 0) {
+            // Don't know the binning; assume it is unity
+            sourceBin = binning->nYbin;
+        }
+
+        psTrace("psModules.camera", 3, "CELL.Y0: Before: %d After: %d\n", yZero, yZero + (ySize - 1) * yParity * sourceBin);
+        psTrace("psModules.camera", 9, "(yParity: %d yBin: %d numRows: %d)\n", yParity, sourceBin, ySize);
+
+        if (yParity == 0 || ySize == 0) {
+            psWarning("New CELL.Y0 may be incorrect due to missing concepts "
+                      "(CELL.Y0, CELL.YPARITY, CELL.YBIN, CELL.YSIZE)");
+        }
+
+        yZero += (ySize - 1) * yParity * sourceBin; // Change the parity on the Y0 position
+        psMetadataItem *newItem = psMetadataLookup(target->concepts, "CELL.Y0"); // CELL.Y0 from target
+        newItem->data.S32 = yZero;
+    }
+
+    // Update the binning concepts
+    // XXX this should probably be done with a common function using psImageBinning
+    psMetadataItem *binItem = psMetadataLookup(target->concepts, "CELL.XBIN");
+    binItem->data.S32 *= xBin;
+    binItem = psMetadataLookup(target->concepts, "CELL.YBIN");
+    binItem->data.S32 *= yBin;
+
+    // Copy any headers
+    pmHDU *targetHDU = pmHDUFromCell(target); // The target HDU
+    if (targetHDU) {
+        pmHDU *sourceHDU = pmHDUFromCell(source); // The source HDU
+        if (!sourceHDU) {
+            psWarning("Unable to copy header: no source header.");
+        } else if (sourceHDU->header) {
+            targetHDU->header = psMetadataCopy(targetHDU->header, sourceHDU->header);
+        }
+    }
+
+    // Copy the PHU over as well, if required
+    pmHDU *targetPHU = findBlankPHU(target); // The target PHU
+    if (targetPHU && targetPHU != targetHDU) {
+        // pmHDU *sourcePHU = pmHDUGetHighest(source->parent->parent, source->parent, source); // A source HDU
+        pmHDU *sourcePHU = findBlankPHU(source); // The target PHU
+        if (sourcePHU && sourcePHU->header) {
+            targetPHU->header = psMetadataCopy(targetPHU->header, sourcePHU->header);
+        }
+    }
+
+    psFree (binning);
+    target->data_exists = true;
+    target->parent->data_exists = true;
+    return true;
+}
+
+// Common engine for pmChipCopy and pmChipCopyStructure
+// Iterate on the components
+static bool chipCopy(pmChip *target,          // The target chip
+                     const pmChip *source, // The source chip, to be copied
+                     bool pixels,             // Copy the pixels?
+                     int xBin, int yBin       // (Relative) binning factors in x and y
+                    )
+{
+    assert(target);
+    assert(source);
+    assert(xBin > 0);
+    assert(yBin > 0);
+
+    if (!source->data_exists) {
+        // Copied everything that exists
+        return true;
+    }
+
+    psArray *targetCells = target->cells; // The target cells
+    psArray *sourceCells = source->cells; // The source cells
+    if (targetCells->n != sourceCells->n) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Number of source cells (%ld) differs from the number of target cells (%ld)\n",
+                sourceCells->n, targetCells->n);
+        return false;
+    }
+
+    bool status = true;                 // Status of copy
+    for (int i = 0; i < targetCells->n; i++) {
+        pmCell *targetCell = targetCells->data[i]; // The target cell
+        const char *cellName = psMetadataLookupStr(NULL, targetCell->concepts, "CELL.NAME"); // Name of cell
+        int cellNum = pmChipFindCell(source, cellName); // Number of cell with that name
+        if (cellNum >= 0) {
+            pmCell *sourceCell = sourceCells->data[cellNum]; // The source cell
+            status &= cellCopy(targetCell, sourceCell, pixels, xBin, yBin);
+        }
+    }
+
+    // Update the concepts
+    psMetadataItem *chipName = psMemIncrRefCounter(psMetadataLookup(target->concepts, "CHIP.NAME"));
+    psMetadataCopy(target->concepts, source->concepts);
+    psMetadataAddItem(target->concepts, chipName, PS_LIST_TAIL, PS_META_REPLACE);
+    psFree(chipName);
+    psMetadataCopy(target->parent->concepts, source->parent->concepts);
+
+    target->data_exists = true;
+    return status;
+}
+
+// create a new pmChip with the data derived from the supplied chip
+pmChip *pmChipDuplicate(pmFPA *fpa, const pmChip *sourceChip)
+{
+    assert(sourceChip);
+
+    bool status;
+    char *chipName = psMetadataLookupStr(&status, sourceChip->concepts, "CHIP.NAME");
+    pmChip *targetChip = pmChipAlloc (NULL, chipName);
+    targetChip->parent = fpa;
+
+    psArray *sourceCells = sourceChip->cells; // The source cells
+
+    for (int i = 0; i < sourceCells->n; i++) {
+        pmCell *sourceCell = sourceCells->data[i]; // The sources cell
+        const char *cellName = psMetadataLookupStr(NULL, sourceCell->concepts, "CELL.NAME"); // Name of cell
+        // XXX are there other concepts I need to copy first?
+        pmCell *targetCell = pmCellAlloc (targetChip, cellName);
+        int xParityTarget = psMetadataLookupS32(&status, sourceCell->concepts, "CELL.XPARITY"); // Target x parity
+        psMetadataAddS32 (targetCell->concepts, PS_LIST_TAIL, "CELL.XPARITY", PS_META_REPLACE, "", xParityTarget);
+        int yParityTarget = psMetadataLookupS32(&status, sourceCell->concepts, "CELL.YPARITY"); // Target y parity
+        psMetadataAddS32 (targetCell->concepts, PS_LIST_TAIL, "CELL.YPARITY", PS_META_REPLACE, "", yParityTarget);
+        if (!cellCopy(targetCell, sourceCell, true, 1, 1)) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, false, "failed to duplicate chip\n");
+            return NULL;
+        }
+        // update the attributes
+        targetCell->file_exists = sourceCell->file_exists;
+        targetCell->data_exists = sourceCell->data_exists;
+        targetCell->process     = sourceCell->process;
+    }
+
+    // Update the concepts
+    psMetadataCopy(targetChip->concepts, sourceChip->concepts);
+
+    // update the attributes
+    targetChip->file_exists = sourceChip->file_exists;
+    targetChip->data_exists = sourceChip->data_exists;
+    targetChip->process     = sourceChip->process;
+
+    return targetChip;
+}
+
+// Common engine for pmFPACopy and pmFPACopyStructure.
+// Iterate on the components
+static bool fpaCopy(pmFPA *target,      // The target FPA
+                    const pmFPA *source, // The source FPA, to be copied
+                    bool pixels,        // Copy the pixels?
+                    int xBin, int yBin  // (Relative) binning factors in x and y
+                   )
+{
+    assert(target);
+    assert(source);
+    assert(xBin > 0);
+    assert(yBin > 0);
+
+    psArray *targetChips = target->chips; // The target chips
+    psArray *sourceChips = source->chips; // The source chips
+    if (targetChips->n != sourceChips->n) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Number of source chips (%ld) differs from the number of target chips (%ld)\n",
+                sourceChips->n, targetChips->n);
+        return false;
+    }
+
+    bool status = true;                 // Status of copy
+    for (int i = 0; i < targetChips->n; i++) {
+        pmChip *targetChip = targetChips->data[i]; // The target chip
+        const char *chipName = psMetadataLookupStr(NULL, targetChip->concepts, "CHIP.NAME"); // Name of chip
+        int chipNum = pmFPAFindChip(source, chipName); // Number of chip with that name
+        if (chipNum >= 0) {
+            pmChip *sourceChip = sourceChips->data[chipNum]; // The source chip
+            status &= chipCopy(targetChip, sourceChip, pixels, xBin, yBin);
+        }
+    }
+
+    // Update the concepts
+    psMetadataCopy(target->concepts, source->concepts);
+
+    return status;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmFPACopy(pmFPA *target, const pmFPA *source)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    if (target == source) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy FPA onto itself.");
+        return false;
+    }
+    return fpaCopy(target, source, true, 1, 1);
+}
+
+bool pmChipCopy(pmChip *target, const pmChip *source)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    if (target == source) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy chip onto itself.");
+        return false;
+    }
+    return chipCopy(target, source, true, 1, 1);
+}
+
+bool pmCellCopy(pmCell *target, const pmCell *source)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    if (target == source) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy cell onto itself.");
+        return false;
+    }
+    return cellCopy(target, source, true, 1, 1);
+}
+
+
+bool pmFPACopyStructure(pmFPA *target, const pmFPA *source, int xBin, int yBin)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_INT_POSITIVE(xBin, false);
+    PS_ASSERT_INT_POSITIVE(yBin, false);
+    if (target == source) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy FPA onto itself.");
+        return false;
+    }
+    return fpaCopy(target, source, false, xBin, yBin);
+}
+
+bool pmChipCopyStructure(pmChip *target, const pmChip *source, int xBin, int yBin)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_INT_POSITIVE(xBin, false);
+    PS_ASSERT_INT_POSITIVE(yBin, false);
+    if (target == source) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy chip onto itself.");
+        return false;
+    }
+    return chipCopy(target, source, false, xBin, yBin);
+}
+
+bool pmCellCopyStructure(pmCell *target, const pmCell *source, int xBin, int yBin)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_INT_POSITIVE(xBin, false);
+    PS_ASSERT_INT_POSITIVE(yBin, false);
+    if (target == source) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Can't copy cell onto itself.");
+        return false;
+    }
+    return cellCopy(target, source, false, xBin, yBin);
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPACopy.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPACopy.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPACopy.h	(revision 15358)
@@ -0,0 +1,78 @@
+/* @file pmFPACopy.h
+ * @brief Functions to copy FPA components.
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-04-12 02:51:44 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_COPY_H
+#define PM_FPA_COPY_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Copy an FPA and components, including the pixels, to a different representation of the same camera
+///
+/// This function is useful for converting between different representations of the same camera.  For example,
+/// between Megacam "RAW" (one amp per extension) and Megacam "SPLICED" formats (two amps = 1 chip per
+/// extension, spliced together).  Components are spliced together as necessary.
+bool pmFPACopy(pmFPA *target,           ///< The target FPA
+               const pmFPA *source      ///< The source FPA, to be copied
+              );
+
+/// Copy a chip and components, including the pixels, to a different representation of the same camera
+///
+/// This function is useful for converting between different representations of the same camera.  For example,
+/// between Megacam "RAW" (one amp per extension) and Megacam "SPLICED" formats (two amps = 1 chip per
+/// extension, spliced together).  Components are spliced together as necessary.
+bool pmChipCopy(pmChip *target,         ///< The target chip
+                const pmChip *source    ///< The source chip, to be copied
+               );
+
+/// Copy a cell and components, including the pixels, to a different representation of the same camera
+///
+/// This function is useful for converting between different representations of the same camera.  For example,
+/// between Megacam "RAW" (one amp per extension) and Megacam "SPLICED" formats (two amps = 1 chip per
+/// extension, spliced together).  Components are spliced together as necessary.
+bool pmCellCopy(pmCell *target,         ///< The target cell
+                const pmCell *source    ///< The source cell, to be copied
+               );
+
+
+/// Copy an FPA, but not the pixels, to a different representation of the same camera
+///
+/// This function the same as pmFPACopy, except that the pixels are not copied (though images of sufficient
+/// size are allocated in the target).  Changes the CELL.XBIN and CELL.YBIN according to the provided binning
+/// factors.
+bool pmFPACopyStructure(pmFPA *target,   ///< The target FPA
+                        const pmFPA *source, ///< The source FPA, to be copied
+                        int xBin, int yBin ///< Binning factors in x and y
+                       );
+
+/// Copy a chip, but not the pixels, to a different representation of the same camera
+///
+/// This function the same as pmChipCopy, except that the pixels are not copied (though images of sufficient
+/// size are allocated in the target).  Changes the CELL.XBIN and CELL.YBIN according to the provided binning
+/// factors.
+bool pmChipCopyStructure(pmChip *target, ///< The target chip
+                         const pmChip *source, ///< The source chip, to be copied
+                         int xBin, int yBin ///< Binning factors in x and y
+                        );
+
+/// Copy a cell, but not the pixels, to a different representation of the same camera
+///
+/// This function the same as pmCellCopy, except that the pixels are not copied (though images of sufficient
+/// size are allocated in the target).  Changes the CELL.XBIN and CELL.YBIN according to the provided binning
+/// factors.
+bool pmCellCopyStructure(pmCell *target, ///< The target cell
+                         const pmCell *source, ///< The source cell, to be copied
+                         int xBin, int yBin ///< Binning factors in x and y
+                        );
+
+pmChip *pmChipDuplicate(pmFPA *fpa, const pmChip *source);
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAExtent.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAExtent.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAExtent.c	(revision 15358)
@@ -0,0 +1,155 @@
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+
+// return cell pixels bounding the readout
+psRegion *pmReadoutExtent(const pmReadout *readout)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, NULL);
+
+    psImage *image = readout->image;    // Image from which to get dimensions
+    if (!image) {
+        image = readout->mask;
+    }
+    if (!image) {
+        image = readout->weight;
+    }
+
+    int xSize = 0;
+    int ySize = 0;
+
+    if (!image) {
+	pmHDU *hdu = pmHDUFromReadout (readout);
+	if (hdu && hdu->header) {
+	    xSize = psMetadataLookupS32(NULL, hdu->header, "NAXIS1");
+	    ySize = psMetadataLookupS32(NULL, hdu->header, "NAXIS2");
+	} else {
+        // Don't have anything to base the true extent on, so have to give the hardwired value (largest possible extent)
+	    xSize = psMetadataLookupS32(NULL, readout->parent->concepts, "CELL.XSIZE");
+	    ySize = psMetadataLookupS32(NULL, readout->parent->concepts, "CELL.YSIZE");
+	}
+        return psRegionAlloc(0, xSize, 0, ySize);
+    }
+
+    // Get the offset to the CCD window
+    int xWindow = psMetadataLookupS32(NULL, readout->parent->concepts, "CELL.XWINDOW");
+    int yWindow = psMetadataLookupS32(NULL, readout->parent->concepts, "CELL.YWINDOW");
+    return psRegionAlloc(xWindow, xWindow + image->numCols,
+                         yWindow, yWindow + image->numRows);
+}
+
+// return chip pixels bounding the cell (all readouts)
+psRegion *pmCellExtent(const pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+
+    psArray *readouts = cell->readouts; // Array of component readouts
+    psRegion *cellExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of cell
+    for (long i = 0; i < readouts->n; i++) {
+        pmReadout *readout = readouts->data[i]; // Readout of interest
+        psRegion *roExtent = pmReadoutExtent(readout); // Extent of readout
+        cellExtent->x0 = PS_MIN(cellExtent->x0, roExtent->x0);
+        cellExtent->x1 = PS_MAX(cellExtent->x1, roExtent->x1);
+        cellExtent->y0 = PS_MIN(cellExtent->y0, roExtent->y0);
+        cellExtent->y1 = PS_MAX(cellExtent->y1, roExtent->y1);
+        psFree(roExtent);
+    }
+
+    bool mdok;                          // Status of MD lookup
+    int x0 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.X0"); // Cell x offset
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find CELL.X0.\n");
+        psFree(cellExtent);
+        return NULL;
+    }
+    int y0 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.Y0"); // Cell y offset
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find CELL.Y0.\n");
+        psFree(cellExtent);
+        return NULL;
+    }
+
+    cellExtent->x0 += x0;
+    cellExtent->x1 += x0;
+    cellExtent->y0 += y0;
+    cellExtent->y1 += y0;
+
+    return cellExtent;
+}
+
+// return chip pixels included in all cells
+psRegion *pmChipPixels(const pmChip *chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    psArray *cells = chip->cells;       // Array of component cells
+    psRegion *chipExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of chip
+    for (long i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // Cell of interest
+        psRegion *cellExtent = pmCellExtent(cell); // Extent of cell
+        chipExtent->x0 = PS_MIN(chipExtent->x0, cellExtent->x0);
+        chipExtent->x1 = PS_MAX(chipExtent->x1, cellExtent->x1);
+        chipExtent->y0 = PS_MIN(chipExtent->y0, cellExtent->y0);
+        chipExtent->y1 = PS_MAX(chipExtent->y1, cellExtent->y1);
+        psFree(cellExtent);
+    }
+
+    return chipExtent;
+}
+
+// return pixels in basic FPA grid bounded by chip
+// this FPA grid has 0,0 at the 0,0 corner of one chip, and is NOT the same
+// as the astrometry focal plane coordinate system
+psRegion *pmChipExtent(const pmChip *chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    psRegion *chipExtent = pmChipPixels(chip);
+    if (!chipExtent) return NULL;
+
+    bool mdok;                          // Status of MD lookup
+    int x0 = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.X0"); // Chip x offset
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find CHIP.X0.\n");
+        psFree(chipExtent);
+        return NULL;
+    }
+    int y0 = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.Y0"); // Chip y offset
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find CHIP.Y0.\n");
+        psFree(chipExtent);
+        return NULL;
+    }
+
+    chipExtent->x0 += x0;
+    chipExtent->x1 += x0;
+    chipExtent->y0 += y0;
+    chipExtent->y1 += y0;
+
+    return chipExtent;
+}
+
+// return FPA pixels included in all chips
+// this FPA grid has 0,0 at the 0,0 corner of one chip, and is NOT the same
+// as the astrometry focal plane coordinate system
+psRegion *pmFPAPixels(const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    psArray *chips = fpa->chips;       // Array of component chips
+    psRegion *fpaExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of fpa
+    for (long i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        psRegion *chipExtent = pmChipExtent(chip); // Extent of chip
+        fpaExtent->x0 = PS_MIN(fpaExtent->x0, chipExtent->x0);
+        fpaExtent->x1 = PS_MAX(fpaExtent->x1, chipExtent->x1);
+        fpaExtent->y0 = PS_MIN(fpaExtent->y0, chipExtent->y0);
+        fpaExtent->y1 = PS_MAX(fpaExtent->y1, chipExtent->y1);
+        psFree(chipExtent);
+    }
+
+    return fpaExtent;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAExtent.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAExtent.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAExtent.h	(revision 15358)
@@ -0,0 +1,49 @@
+/* @file  pmPFAExtent.h
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-21 22:01:32 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_EXTENT_H
+#define PM_FPA_EXTENT_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Return the extent of a readout
+///
+/// The extent is determined from an image, if present, or the CELL.TRIMSEC otherwise.
+psRegion *pmReadoutExtent(const pmReadout *readout ///< The readout of interest
+                         );
+
+/// Return the extent of a cell
+///
+/// The extent is determined from the extent of the component readouts, plus CELL.X0,Y0
+psRegion *pmCellExtent(const pmCell *cell ///< The cell of interest
+                      );
+
+// return chip pixels included in all cells
+psRegion *pmChipPixels(const pmChip *chip);
+
+/// Return the extent of a chip
+///
+/// The extent is determined from the extent of the component cells, plus CHIP.X0,Y0
+psRegion *pmChipExtent(const pmChip *chip ///< The chip of interest
+                      );
+
+// return FPA pixels included in all chips
+// this FPA grid has 0,0 at the 0,0 corner of one chip, and is NOT the same
+// as the astrometry focal plane coordinate system
+psRegion *pmFPAPixels(const pmFPA *fpa);
+
+/// Return the extent of an FPA
+///
+/// The extent is determined from the extent of the component chips.
+psRegion *pmFPAExtent(const pmFPA *fpa ///< The FPA of interest
+                     );
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAFlags.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAFlags.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAFlags.c	(revision 15358)
@@ -0,0 +1,341 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAFlags.h"
+
+
+/** functions to turn on/off the file_exists flag **/
+bool pmFPASetFileStatus(pmFPA *fpa, bool status)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        pmChipSetFileStatus (chip, status);
+    }
+    return true;
+}
+
+bool pmChipSetFileStatus(pmChip *chip, bool status)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+
+    chip->file_exists = status;
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        pmCellSetFileStatus (cell, status);
+    }
+    return true;
+}
+
+bool pmCellSetFileStatus(pmCell *cell, bool status)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+
+    cell->file_exists = status;
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        readout->file_exists = status;
+    }
+    return true;
+}
+
+bool pmFPACheckFileStatus(const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!pmChipCheckFileStatus(chip)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+bool pmChipCheckFileStatus(const pmChip *chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    if (!chip->file_exists) {
+        return false;
+    }
+
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        if (!pmCellCheckFileStatus(cell)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+bool pmCellCheckFileStatus(const pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    if (!cell->file_exists) {
+        return false;
+    }
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        if (!readout->file_exists) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/** functions to turn on/off the data_exists flag **/
+bool pmFPASetDataStatus(pmFPA *fpa, bool status)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        pmChipSetDataStatus (chip, status);
+    }
+    return true;
+}
+
+bool pmChipSetDataStatus(pmChip *chip, bool status)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+
+    chip->data_exists = status;
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        pmCellSetDataStatus (cell, status);
+    }
+    return true;
+}
+
+bool pmCellSetDataStatus (pmCell *cell, bool status)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+
+    cell->data_exists = status;
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        readout->data_exists = status;
+    }
+    return true;
+}
+
+// pmFPA does not have its own data_exists flag; check its children
+bool pmFPACheckDataStatus (const pmFPA *fpa) {
+
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+	pmChip *chip = fpa->chips->data[i];
+	if (chip == NULL) continue;
+	if (chip->data_exists) return true;
+    }
+    return false;
+}
+
+bool pmChipCheckDataStatus (const pmChip *chip) {
+
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+
+    return (chip->data_exists);
+}
+
+bool pmCellCheckDataStatus (const pmCell *cell) {
+
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+
+    return (cell->data_exists);
+}
+
+bool pmReadoutCheckDataStatus (const pmReadout *readout) {
+
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    return (readout->data_exists);
+}
+
+bool pmFPAviewCheckDataStatus (const pmFPA *fpa, const pmFPAview *view) {
+
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    if (view->chip == -1) {
+	bool exists = pmFPACheckDataStatus (fpa);
+	return exists;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+	psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
+	return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+	bool exists = pmChipCheckDataStatus (chip);
+	return exists;
+    }
+
+    if (view->cell >= chip->cells->n) {
+	psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
+	return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+	bool exists = pmCellCheckDataStatus (cell);
+	return exists;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+	psError(PS_ERR_IO, true, "Requested readout == %d >= cell->readouds->n == %ld", view->readout, cell->readouts->n);
+	return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    bool exists = pmReadoutCheckDataStatus (readout);
+    return exists;
+}
+
+// Set cells within a chip to be processed or not
+static bool setCellsProcess(const pmChip *chip, // Chip of interest
+                            bool process  // Process this chip?
+                           )
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+
+    psArray *cells = chip->cells;       // Component cells
+    if (! cells) {
+        return false;
+    }
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *tmpCell = cells->data[i]; // Cell of interest
+        if (tmpCell) {
+            tmpCell->process = process;
+        }
+    }
+
+    return true;
+}
+
+
+bool pmFPASelectChip(pmFPA *fpa, int chipNum, bool exclusive)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    psArray *chips = fpa->chips;        // Component chips
+    if ((chips == NULL) || (chipNum >= chips->n)) {
+        return(false);
+    }
+
+    for (int i = 0 ; i < chips->n ; i++) {
+        pmChip *tmpChip = (pmChip *) chips->data[i];
+        if (tmpChip == NULL) {
+            continue;
+        }
+        if (i == chipNum) {
+            tmpChip->process = true;
+            setCellsProcess(tmpChip, true);
+        } else {
+            if (exclusive) {
+                tmpChip->process = false;
+                setCellsProcess(tmpChip, false);
+            }
+        }
+
+    }
+
+    return true;
+}
+
+// XXX this function should probably be re-defined to merge with 'setCellsProcess'
+bool pmChipSelectCell(pmChip *chip, int cellNum, bool exclusive)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+
+    psArray *cells = chip->cells;       // Component cells
+    if (!cells || cellNum > cells->n) {
+        return false;
+    }
+
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];
+        if (!cell) {
+            continue;
+        }
+        if (i == cellNum) {
+            cell->process = true;
+        } else {
+            if (exclusive) {
+                cell->process = false;
+            }
+        }
+    }
+    return true;
+}
+
+
+int pmFPAExcludeChip(pmFPA *fpa, int chipNum)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, -1);
+
+    psArray *chips = fpa->chips;        // Component chips
+    if (chips == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: fpa->chips == NULL\n");
+        return(0);
+    }
+    if ((chipNum >= chips->n) || (NULL == (pmChip *) chips->data[chipNum])) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: the specified chip (%d) does not exist.\n", chipNum);
+        return(0);
+    }
+
+    int numChips = 0;                   // Number of chips to be processed
+    for (int i = 0 ; i < chips->n ; i++) {
+        pmChip *tmpChip = (pmChip *) chips->data[i]; // Chip of interest
+        if (tmpChip != NULL) {
+            if (i == chipNum) {
+                tmpChip->process = false;
+                setCellsProcess(tmpChip, false); // Wipe out the cell as well
+            } else if (tmpChip->process) {
+                numChips++;
+            }
+        }
+    }
+
+    return(numChips);
+}
+
+
+int pmChipExcludeCell(pmChip *chip, int cellNum)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, -1);
+
+    psArray *cells = chip->cells;       // The component cells
+    if (!cells || cellNum > cells->n) {
+        return 0;
+    }
+
+    int numCells = 0;                   // Number of cells to be processed
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];
+        if (!cell) {
+            continue;
+        }
+        if (i == cellNum) {
+            cell->process = false;
+        } else {
+            numCells++;
+        }
+    }
+
+    return numCells;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAFlags.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAFlags.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAFlags.h	(revision 15358)
@@ -0,0 +1,122 @@
+/*  @file pmFPAFlags.h
+ *  @brief Functions for setting and checking the status flags within the FPA hierarchy
+ * 
+ *  @author George Gusciora, MHPCC
+ *  @author Paul Price, IfA
+ *  @author Eugene Magnier, IfA
+ * 
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-05-03 20:04:01 $
+ *  Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_FLAGS_H
+#define PM_FPA_FLAGS_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+// Functions to turn on/off the file_exists flags
+
+/// Set the file_exists flag for an FPA and components
+bool pmFPASetFileStatus(pmFPA *fpa,     ///< FPA for which to set status
+                        bool status     ///< Status to set
+                       );
+
+/// Set the file_exists flag for a chip and components
+bool pmChipSetFileStatus(pmChip *chip,  ///< Chip for which to set status
+                         bool status    ///< Status to set
+                        );
+
+/// Set the file_exists flag for a cell and components
+bool pmCellSetFileStatus(pmCell *cell,  ///< Cell for which to set status
+                         bool status    ///< Status to set
+                        );
+
+// Functions to check the file_exists flags
+
+/// Return the file_exists status for an FPA and components
+bool pmFPACheckFileStatus(const pmFPA *fpa ///< FPA for which to check status
+                         );
+
+/// Return the file_exists status for a chip and components
+bool pmChipCheckFileStatus(const pmChip *chip ///< Chip for which to check status
+                          );
+
+/// Return the file_exists status for a chip and components
+bool pmCellCheckFileStatus(const pmCell *cell ///< Cell for which to check status
+                          );
+
+// Functions to turn on/off the data_exists flags
+
+/// Set the data_exists flag for an FPA and components
+bool pmFPASetDataStatus(pmFPA *fpa,     ///< FPA for which to set status
+                        bool status     ///< Status to set
+                       );
+
+/// Set the data_exists flag for a chip and components
+bool pmChipSetDataStatus(pmChip *chip,  ///< Chip for which to set status
+                         bool status    ///< Status to set
+                        );
+
+/// Set the data_exists flag for a cell and components
+bool pmCellSetDataStatus(pmCell *cell,  ///< Cell for which to set status
+                         bool status    ///< Status to set
+                        );
+
+
+// Functions the check the data_exists flags
+
+/// Check data_exists for the element of this fpa at this view
+bool pmFPAviewCheckDataStatus (const pmFPA *fpa, ///< FPA to check
+			       const pmFPAview *view ///< check for this view 
+  );
+
+/// Check data_exists for this fpa
+bool pmFPACheckDataStatus (const pmFPA *fpa ///< FPA to check
+  );
+
+/// Check data_exists for this chip
+bool pmChipCheckDataStatus (const pmChip *chip ///< Chip to check
+);
+
+/// Check data_exists for this cell
+bool pmCellCheckDataStatus (const pmCell *cell ///< Cell to check
+);
+
+/// Check data_exists for this readout
+bool pmReadoutCheckDataStatus (const pmReadout *readout ///< Readout to check
+);
+
+
+// Functions to set the process flags
+
+/// Select a chip within an FPA for processing
+///
+/// If exclusive is true, the specified chip is the only chip to be processed.  A negative value for chipNum
+/// is valid and, in combinations with exclusive, de-selects all chips.
+bool pmFPASelectChip(pmFPA *fpa,        ///< FPA containing the chip of interest
+                     int chipNum,       ///< Chip number to select
+                     bool exclusive     ///< Process this chip exclusive of the others?
+                    );
+
+/// Select a chip within a chip for processing
+///
+/// If exclusive is true, the specified cell is the only chip to be processed.  A negative value for cellNum
+/// is valid and, in combinations with exclusive, de-selects all chips.
+bool pmChipSelectCell(pmChip *chip,     ///< Chip containing the cell of interest
+                      int cellNum,      ///< Cell number to select
+                      bool exclusive    ///< Process this cell exclusive of the others?
+                     );
+
+/// Exclude a chip within an FPA from processing
+int pmFPAExcludeChip(pmFPA *fpa,        ///< FPA containing the chip of interest
+                     int chipNum        ///< Chip number to exclude
+                    );
+
+/// Exclude a cell within a chip from processing
+int pmChipExcludeCell(pmChip *chip,     ///< Chip containing the chip of interest
+                      int cellNum       ///< Cell number to exclude
+                     );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAHeader.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAHeader.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAHeader.c	(revision 15358)
@@ -0,0 +1,77 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmConcepts.h"
+#include "pmFPAHeader.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmCellReadHeader(pmCell *cell, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    if (!cell->hdu) {
+        return pmChipReadHeader(cell->parent, fits);
+    }
+    if (!pmHDUReadHeader(cell->hdu, fits)) {
+        psError(PS_ERR_IO, false, "Unable to read header for cell.\n");
+        return false;
+    }
+
+    return pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_HEADER, false, NULL);
+}
+
+
+bool pmChipReadHeader(pmChip *chip, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    if (!chip->hdu) {
+        return pmFPAReadHeader(chip->parent, fits);
+    }
+    if (!pmHDUReadHeader(chip->hdu, fits)) {
+        psError(PS_ERR_IO, false, "Unable to read header for cell.\n");
+        return false;
+    }
+
+    if (!pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_HEADER, true, true, NULL)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts for chip.\n");
+        return false;
+    }
+
+    return true;
+}
+
+
+bool pmFPAReadHeader(pmFPA *fpa, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    if (!fpa->hdu) {
+        return false;
+    }
+    if (!pmHDUReadHeader(fpa->hdu, fits)) {
+        psError(PS_ERR_IO, false, "Unable to read header for cell.\n");
+        return false;
+    }
+
+    if (!pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_HEADER, true, NULL)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read concepts for FPA.\n");
+        return false;
+    }
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAHeader.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAHeader.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAHeader.h	(revision 15358)
@@ -0,0 +1,41 @@
+/*  @file pmFPAHeader.h
+ *  @brief Functions read FITS headers for FPA components
+ * 
+ *  @author Paul Price, IfA
+ * 
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-01-24 02:54:14 $
+ *  Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_HEADER_H
+#define PM_FPA_HEADER_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Read the FITS header (and ingest concepts) for an FPA, if it exists at this level
+///
+/// Returns false if there was a problem.  Returns true if it successfully read the header, or if the header
+/// was already there.  No iteration to lower levels is performed.
+bool pmFPAReadHeader(pmFPA *fpa,        ///< FPA for which to read header
+                     psFits *fits       ///< FITS file handle
+                    );
+
+/// Read the FITS header (and ingest concepts) for a chip, if it exists at this level
+///
+/// Returns false if there was a problem.  Returns true if it successfully read the header, or if the header
+/// was already there.  No iteration to lower levels is performed.
+bool pmChipReadHeader(pmChip *chip,     ///< Chip for which to read header
+                      psFits *fits      ///< FITS file handle
+                     );
+
+/// Read the FITS header (and ingest concepts) for a cell, if it exists at this level
+///
+/// Returns false if there was a problem.  Returns true if it successfully read the header, or if the header
+/// was already there.  No iteration to lower levels is performed.
+bool pmCellReadHeader(pmCell *cell,     ///< Cell for which to read header
+                      psFits *fits      ///< FITS file handle
+                     );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPALevel.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPALevel.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPALevel.c	(revision 15358)
@@ -0,0 +1,60 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <pslib.h>
+
+#include "pmFPALevel.h"
+
+static const char *NameNONE = "NONE";   ///< Name for PM_FPA_LEVEL_NONE
+static const char *NameFPA = "FPA";     ///< Name for PM_FPA_LEVEL_FPA
+static const char *NameCHIP = "CHIP";   ///< Name for PM_FPA_LEVEL_CHIP
+static const char *NameCELL = "CELL";   ///< Name for PM_FPA_LEVEL_CELL
+static const char *NameREADOUT = "READOUT"; ///< Name for PM_FPA_LEVEL_READOUT
+
+const char *pmFPALevelToName(pmFPALevel level)
+{
+    switch (level) {
+    case PM_FPA_LEVEL_NONE:
+        return NameNONE;
+    case PM_FPA_LEVEL_FPA:
+        return NameFPA;
+    case PM_FPA_LEVEL_CHIP:
+        return NameCHIP;
+    case PM_FPA_LEVEL_CELL:
+        return NameCELL;
+    case PM_FPA_LEVEL_READOUT:
+        return NameREADOUT;
+    default:
+        psAbort("You can't get here; level = %d", level);
+    }
+    return NULL;
+}
+
+pmFPALevel pmFPALevelFromName(const char *name)
+{
+    pmFPALevel val;
+
+    if (name == NULL) {
+        val = PM_FPA_LEVEL_NONE;
+    } else if (!strcasecmp(name, "FPA"))     {
+        val = PM_FPA_LEVEL_FPA;
+    } else if (!strcasecmp(name, "CHIP"))    {
+        val = PM_FPA_LEVEL_CHIP;
+    } else if (!strcasecmp(name, "CELL"))    {
+        val = PM_FPA_LEVEL_CELL;
+    } else if (!strcasecmp(name, "READOUT")) {
+        val = PM_FPA_LEVEL_READOUT;
+    } else if (!strcasecmp(name, "NONE")) {
+        val = PM_FPA_LEVEL_NONE;
+    } else {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised FPA level name: %s", name);
+        val = PM_FPA_LEVEL_NONE;
+    }
+
+    return val;
+}
+
+
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPALevel.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPALevel.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPALevel.h	(revision 15358)
@@ -0,0 +1,35 @@
+/* @file pmFPALevel.h
+ * @brief Defines enum and string representations for the FPA levels
+ *
+ * @author Eugene Magnier, MHPCC
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:14 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_LEVEL_H
+#define PM_FPA_LEVEL_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Specify the level of the FPA hierarchy
+typedef enum {
+    PM_FPA_LEVEL_NONE,                  ///< No particular level specified
+    PM_FPA_LEVEL_FPA,                   ///< Level corresponds to an FPA
+    PM_FPA_LEVEL_CHIP,                  ///< Level corresponds to a Chip
+    PM_FPA_LEVEL_CELL,                  ///< Level corresponds to a Cell
+    PM_FPA_LEVEL_READOUT                ///< Level corresponds to a Readout
+} pmFPALevel;
+
+
+/// Return the string representation of the FPA level
+const char *pmFPALevelToName(pmFPALevel level ///< Level enum
+                            );
+
+/// Return the enum representation of the FPA level
+pmFPALevel pmFPALevelFromName(const char *name ///< Level name
+                             );
+/// @}
+#endif // #ifndef PM_FPA_LEVEL_H
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAMaskWeight.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAMaskWeight.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAMaskWeight.c	(revision 15358)
@@ -0,0 +1,312 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+#include "pmHDUGenerate.h"
+#include "pmFPAMaskWeight.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static (private) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Create the parent mask images that reside in the HDU
+static void createParentMasks(pmHDU *hdu // The HDU for which to create
+                             )
+{
+    assert(hdu);
+    assert(hdu->images);
+
+    // Generate the parent mask images
+    psArray *images = hdu->images;      // Array of images
+    psArray *masks = hdu->masks;        // Array of masks
+    if (!masks) {
+        masks = psArrayAlloc(images->n);
+        hdu->masks = masks;
+    }
+
+    for (long i = 0; i < images->n; i++) {
+        psImage *image = images->data[i]; // The image for this readout
+        if (!image || masks->data[i]) {
+            continue;
+        }
+        masks->data[i] = psImageAlloc(image->numCols, image->numRows, PS_TYPE_U8);
+        psImageInit(masks->data[i], 0);
+    }
+
+    return;
+}
+
+// Create the parent weight images that reside in the HDU
+static void createParentWeights(pmHDU *hdu // The HDU for which to create
+                               )
+{
+    assert(hdu);
+    assert(hdu->images);
+
+    // Generate the parent mask images
+    psArray *images = hdu->images;      // Array of images
+    psArray *weights = hdu->weights;    // Array of weight images
+    if (!weights) {
+        weights = psArrayAlloc(images->n);
+        hdu->weights = weights;
+    }
+
+    for (long i = 0; i < images->n; i++) {
+        psImage *image = images->data[i]; // The image for this readout
+        if (!image || weights->data[i]) {
+            continue;
+        }
+        weights->data[i] = psImageAlloc(image->numCols, image->numRows, PS_TYPE_F32);
+    }
+
+    return;
+}
+
+// Identify a readout within the HDU, on the basis of the image pointer.
+// This is a little dirty, but hopefully should work....
+static long identifyReadout(pmHDU *hdu, // The HDU containing the readouts
+                            pmReadout *readout // The readout to be identified
+                           )
+{
+    assert(hdu);
+    assert(readout);
+
+    long index = -1;                    // Index of the readout
+    for (long i = 0; i < hdu->images->n && index == -1; i++) {
+        if (hdu->images->data[i] == readout->image->parent) {
+            index = i;
+        }
+    }
+
+    return index;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmReadoutSetMask(pmReadout *readout, psMaskType satMask, psMaskType badMask)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_IMAGE_NON_NULL(readout->image, false);
+
+    pmCell *cell = readout->parent;     // The parent cell
+    bool mdok = true;                   // Status of MD lookup
+
+    // Get the "concepts" of interest
+    float saturation = psMetadataLookupF32(&mdok, cell->concepts, "CELL.SATURATION"); // Saturation level
+    if (!mdok || isnan(saturation)) {
+        psError(PS_ERR_IO, true, "CELL.SATURATION is not set --- unable to set mask.\n");
+        return false;
+    }
+    float bad = psMetadataLookupF32(&mdok, cell->concepts, "CELL.BAD"); // Bad level
+    if (!mdok || isnan(bad)) {
+        psError(PS_ERR_IO, true, "CELL.BAD is not set --- unable to set mask.\n");
+        return false;
+    }
+    psTrace("psModules.camera", 5, "Saturation: %f, bad: %f\n", saturation, bad);
+
+
+    // Set up the mask
+    psImage *image = readout->image;    // The image pixels
+    if (!readout->mask) {
+        // Generate a (throwaway) mask image, if required
+        readout->mask = psImageAlloc(image->numCols, image->numRows, PS_TYPE_U8);
+    }
+    psImage *mask = readout->mask;      // The mask pixels
+    psImageInit(mask, 0);
+
+    // Dereference pointers for speed
+    psF32 **imageData = image->data.F32;// The image
+    psU8  **maskData  = mask->data.U8;  // The mask
+
+    for (int i = 0; i < image->numRows; i++) {
+        for (int j = 0; j < image->numCols; j++) {
+            if (imageData[i][j] >= saturation) {
+                maskData[i][j] |= satMask;
+            }
+            if (imageData[i][j] <= bad) {
+                maskData[i][j] |= badMask;
+            }
+            if (!isfinite(imageData[i][j])) {
+                maskData[i][j] |= badMask;
+            }
+        }
+    }
+
+    return true;
+}
+
+// XXX this function creates the mask pixels, or uses the existing mask
+// pixels.  currently, it will set mask bits if (value <= BAD) or (value >= SATURATION)
+// should we optionally ignore these tests?
+bool pmReadoutGenerateMask(pmReadout *readout, psMaskType satMask, psMaskType badMask)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    pmCell *cell = readout->parent;     // The parent cell
+    bool mdok = true;                   // Status of MD lookup
+
+    // Create the mask image if required
+    if (!readout->mask) {
+        psRegion *trimsec = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.TRIMSEC"); // Trim section
+        if (!mdok || psRegionIsNaN(*trimsec)) {
+            psError(PS_ERR_IO, true, "CELL.TRIMSEC is not set --- unable to set mask.\n");
+            return false;
+        }
+
+        pmHDU *hdu = pmHDUFromCell(cell);   // The HDU containing the cell's pixels
+        PS_ASSERT_PTR_NON_NULL(hdu, false);
+        if (!hdu->images && !pmHDUGenerateForCell(cell)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to generate HDU for cell.\n");
+            return false;
+        }
+
+        createParentMasks(hdu);
+
+        // Need to identify which readout we're working with....
+        long index = identifyReadout(hdu, readout); // Index of the readout
+        if (index == -1) {
+            psError(PS_ERR_UNKNOWN, true, "Unable to identify readout image in HDU.\n");
+            return false;
+        }
+
+        psImage *mask = psImageSubset(hdu->masks->data[index], *trimsec); // The mask pixels
+        if (!mask) {
+            psString trimsecString = psRegionToString(*trimsec);
+            psError(PS_ERR_UNKNOWN, false, "Unable to set mask from HDU with trimsec: %s.\n", trimsecString);
+            psFree(trimsecString);
+            return false;
+        }
+        psImageInit(mask, 0);
+        assert (readout->mask == NULL); // or else this is a memory leak.
+        readout->mask = mask;
+    }
+
+    return pmReadoutSetMask(readout, satMask, badMask);
+}
+
+bool pmReadoutSetWeight(pmReadout *readout, bool poisson)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    pmCell *cell = readout->parent;     // The parent cell
+    bool mdok = true;                   // Status of MD lookup
+
+    // Get the "concepts" of interest
+    float gain = psMetadataLookupF32(&mdok, cell->concepts, "CELL.GAIN"); // Cell gain
+    if (!mdok || isnan(gain)) {
+        psError(PS_ERR_IO, true, "CELL.GAIN is not set --- unable to set weight.\n");
+        return false;
+    }
+    float readnoise = psMetadataLookupF32(&mdok, cell->concepts, "CELL.READNOISE"); // Cell read noise
+    if (!mdok || isnan(readnoise)) {
+        psError(PS_ERR_IO, true, "CELL.READNOISE is not set --- unable to set weight.\n");
+        return false;
+    }
+
+    if (poisson) {
+        // Set weight image to the variance in ADU = f/g + rn^2
+        psImage *image = readout->image;    // The image pixels
+        readout->weight = (psImage*)psBinaryOp(readout->weight, image, "/", psScalarAlloc(gain, PS_TYPE_F32));
+
+        // a negative weight is non-sensical. if the image value drops below 1, the weight must be 1.
+        readout->weight = (psImage*)psUnaryOp(readout->weight, readout->weight, "abs");
+        readout->weight = (psImage*)psBinaryOp(readout->weight, readout->weight, "max",
+                                               psScalarAlloc(1, PS_TYPE_F32));
+    } else {
+        // Just use the read noise
+        if (!readout->weight) {
+            readout->weight = psImageAlloc(readout->image->numCols, readout->image->numRows, PS_TYPE_F32);
+        }
+        psImageInit(readout->weight, 0.0);
+    }
+
+    readout->weight = (psImage*)psBinaryOp(readout->weight, readout->weight, "+",
+                                           psScalarAlloc(readnoise*readnoise/gain/gain, PS_TYPE_F32));
+
+    return true;
+}
+
+// this function creates the weight pixels, or uses the existing weight pixels.  it will set
+// the noise pixel values only if the weight image is not supplied
+bool pmReadoutGenerateWeight(pmReadout *readout, bool poisson)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    pmCell *cell = readout->parent;     // The parent cell
+    bool mdok = true;                   // Status of MD lookup
+
+    // Create the weight image if required
+    if (readout->weight)
+        return true;
+
+    psRegion *trimsec = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.TRIMSEC"); // Trim section
+    if (!mdok || psRegionIsNaN(*trimsec)) {
+        psError(PS_ERR_IO, true, "CELL.TRIMSEC is not set --- unable to set weight.\n");
+        return false;
+    }
+
+    pmHDU *hdu = pmHDUFromCell(cell);   // The HDU containing the cell's pixels
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    if (!hdu->images && !pmHDUGenerateForCell(cell)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate HDU for cell.\n");
+        return false;
+    }
+
+    createParentWeights(hdu);
+
+    // Need to identify which readout we're working with....
+    long index = identifyReadout(hdu, readout); // Index of the readout
+    if (index == -1) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to identify readout image in HDU.\n");
+        return false;
+    }
+
+    psImage *weight = psImageSubset(hdu->weights->data[index], *trimsec); // The weight pixels
+    if (!weight) {
+        psString trimsecString = psRegionToString(*trimsec);
+        psError(PS_ERR_UNKNOWN, false, "Unable to set weight from HDU with trimsec: %s.\n",
+                trimsecString);
+        psFree(trimsecString);
+        return false;
+    }
+    psImageInit(weight, 0);
+    readout->weight = weight;
+
+    return pmReadoutSetWeight(readout, poisson);
+}
+
+bool pmReadoutGenerateMaskWeight(pmReadout *readout, psMaskType satMask, psMaskType badMask, bool poisson)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    bool success = true;                // Was everything successful?
+
+    success &= pmReadoutGenerateMask(readout, satMask, badMask);
+    success &= pmReadoutGenerateWeight(readout, poisson);
+
+    return success;
+}
+
+bool pmCellGenerateMaskWeight(pmCell *cell, psMaskType satMask, psMaskType badMask, bool poisson)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+
+    bool success = true;                // Was everything successful?
+    psArray *readouts = cell->readouts; // Array of readouts
+    for (int i = 0; i < readouts->n; i++) {
+        pmReadout *readout = readouts->data[i]; // The readout
+        pmReadoutGenerateMaskWeight(readout, poisson, satMask, badMask);
+    }
+
+    return success;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAMaskWeight.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAMaskWeight.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAMaskWeight.h	(revision 15358)
@@ -0,0 +1,96 @@
+/* @file pmFPAHeader.h
+ * @brief Functions read FITS headers for FPA components
+ *
+ * @author Paul Price, IfA
+ * @author Eugene Magnier, IfA
+ *
+ * @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-06-02 03:51:03 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_MASK_WEIGHT_H
+#define PM_FPA_MASK_WEIGHT_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+#if 0
+/// Pixel mask values
+typedef enum {
+    PM_MASK_CLEAR    = 0x00,            ///< The pixel is not masked
+    PM_MASK_BLANK    = 0x01,            ///< The pixel is blank or has no (valid) data
+    PM_MASK_FLAT     = 0x02,            ///< The pixel is non-positive in the flat-field
+    PM_MASK_DETECTOR = 0x02,            ///< The detector pixel is bad (e.g., bad column, charge trap)
+    PM_MASK_SAT      = 0x04,            ///< The pixel is saturated in the image of interest
+    PM_MASK_BAD      = 0x04,            ///< The pixel is low in the image of interest
+    PM_MASK_RANGE    = 0x04,            ///< The pixel is out of range in the image of interest
+    PM_MASK_CR       = 0x08,            ///< The pixel is probably a CR
+    PM_MASK_SPARE1   = 0x10,            ///< Spare mask value
+    PM_MASK_SPARE2   = 0x20,            ///< Spare mask value
+    PM_MASK_SUSPECT  = 0x40,            ///< The pixel is suspected of being bad, but may not be
+    PM_MASK_MARK     = 0x80,            ///< The pixel is marked as temporarily ignored
+} pmMaskValue;
+#else
+#define PM_MASK_MARK 0x80            ///< The pixel is marked as temporarily ignored
+#define PM_MASK_SAT  0x04            ///< The pixel is saturated in the image of interest
+#endif
+
+/// Set a temporary readout mask using CELL.SATURATION and CELL.BAD
+///
+/// Identifies pixels that are saturated (>= CELL.SATURATION) or bad (<= CELL.BAD).  The mask that is produced
+/// within the readout is temporary --- it is not added to the HDU.  This is intended for when the user is
+/// iterating using pmReadoutReadNext, in which case the HDU can't be generated.
+bool pmReadoutSetMask(pmReadout *readout, ///< Readout for which to set mask
+                      psMaskType sat,   ///< Mask value to give saturated pixels
+                      psMaskType bad    ///< Mask value to give bad (low) pixels
+    );
+
+
+/// Set a temporary readout weight map using CELL.GAIN and CELL.READNOISE
+///
+/// Calculates weights (actually variances) for each pixel using photon statistics and the cell gain
+/// (CELL.GAIN) and read noise (CELL.READNOISE).  The weight map that is produced within the readout is
+/// temporary --- it is not added to the HDU.  This is intended for when the user is iterating using
+/// pmReadoutReadNext, in which case the HDU can't be generated.
+bool pmReadoutSetWeight(pmReadout *readout, ///< Readout for which to set weight
+                        bool poisson    ///< Use poisson weights (in addition to read noise)?
+                       );
+
+/// Generate a readout mask (suitable for output) using CELL.SATURATION and CELL.BAD
+///
+/// Identifies pixels that are saturated (>= CELL.SATURATION) or bad (<= CELL.BAD).  The mask that is produced
+/// is suitable for output (complete with HDU entry).  This is intended for most operations.
+bool pmReadoutGenerateMask(pmReadout *readout, ///< Readout for which to generate mask
+                           psMaskType sat, ///< Mask value to give saturated pixels
+                           psMaskType bad ///< Mask value to give bad (low) pixels
+    );
+
+/// Generate a weight map (suitable for output) using CELL.GAIN and CELL.READNOISE
+///
+/// Calculates weights (actually variances) for each pixel using photon statistics and the cell gain
+/// (CELL.GAIN) and read noise (CELL.READNOISE).  The weight map that is produced within the readout is
+/// suitable for output (complete with HDU entry).  This is intended for most operations.
+bool pmReadoutGenerateWeight(pmReadout *readout, ///< Readout for which to generate weight
+                             bool poisson    ///< Use poisson weights (in addition to read noise)?
+                            );
+
+/// Generate mask and weight map for a readout
+///
+/// Calls pmReadoutGenerateMask and pmReadoutGenerateWeight for the readout
+bool pmReadoutGenerateMaskWeight(pmReadout *readout, ///< Readout for which to generate mask and weights
+                                 psMaskType sat, ///< Mask value to give saturated pixels
+                                 psMaskType bad, ///< Mask value to give bad (low) pixels
+                                 bool poisson ///< Use poisson weights (in addition to read noise)?
+                                );
+
+/// Generate mask and weight maps for all readouts within a cell
+///
+/// Calls pmReadoutGenerateMaskWeight for each readout within the cell.
+bool pmCellGenerateMaskWeight(pmCell *cell, ///< Cell for which to generate mask and weights
+                              psMaskType sat, ///< Mask value to give saturated pixels
+                              psMaskType bad, ///< Mask value to give bad (low) pixels
+                              bool poisson ///< Use poisson weights (in addition to read noise)?
+                             );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAMosaic.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAMosaic.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAMosaic.c	(revision 15358)
@@ -0,0 +1,1358 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAFlags.h"
+#include "pmConceptsAverage.h"
+#include "pmHDUUtils.h"
+#include "pmConfig.h"
+#include "pmAstrometryWCS.h"
+#include "pmFPAExtent.h"
+
+#include "pmFPAMosaic.h"
+
+
+#define CELL_LIST_BUFFER 10             // Buffer size for cell lists
+
+#define BLANK_VALUE 0.0                 // Value for pixels that are blank in the mosaicked image (e.g., //
+                                        // between cells).
+                                        // XXX This should ultimately be set to NAN, but psphot doesn't like
+                                        // that (masking needs to be more thorough).
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static (private) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Do two regions overlap?
+#define REGIONS_OVERLAP(region1, region2) \
+((region1->x0 > region2->x0 && region1->x0 < region2->x1) || \
+ (region1->x1 > region2->x0 && region1->x1 < region2->x1) || \
+ (region1->y0 > region2->y0 && region1->y0 < region2->y1) || \
+ (region1->y1 > region2->y0 && region1->y1 < region2->y1))
+
+// Compare a value with a maximum and minimum
+#define COMPARE(value,min,max) \
+if ((value) < (min)) { \
+    (min) = (value); \
+} \
+if ((value) > (max)) { \
+    (max) = (value); \
+}
+
+// Update a concept to the assumed value
+#define FIX_CONCEPT(SOURCE, NAME, TYPE, VALUE) \
+psMetadataItem *item = psMetadataLookup(SOURCE, NAME); \
+item->data.TYPE = VALUE;
+
+// Get the bounds for an chip's pixels on the HDU
+static bool chipBounds(psRegion *bounds, // The bounds for the chip
+                       const pmChip *chip // The chip to examine for contiguity
+                      )
+{
+    assert(chip);
+
+    psArray *cells = chip->cells;       // The array of cells
+    bool mdok = true;                   // Status of MD lookup
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // Cell of interest
+        psRegion *trimsec = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.TRIMSEC"); // Trim section
+        if (!mdok || !trimsec || psRegionIsNaN(*trimsec)) {
+            psError(PS_ERR_UNKNOWN, true, "CELL.TRIMSEC hasn't been set for cell %d.\n", i);
+            return false;
+        }
+
+        if (trimsec->x0 < bounds->x0) {
+            bounds->x0 = trimsec->x0;
+        }
+        if (trimsec->x1 > bounds->x1) {
+            bounds->x1 = trimsec->x1;
+        }
+        if (trimsec->y0 < bounds->y0) {
+            bounds->y0 = trimsec->y0;
+        }
+        if (trimsec->y1 > bounds->y1) {
+            bounds->y1 = trimsec->y1;
+        }
+    }
+
+    return true;
+}
+
+// Make sure the TRIMSEC doesn't overlap with the established image bounds
+static bool chipContiguousTrimsec(psRegion *bounds, // The bounds of the image, altered if primary==true
+                                  const pmChip *chip // The chip to examine for contiguity
+                                 )
+{
+    assert(bounds);
+    assert(chip);
+
+    psArray *cells = chip->cells;       // The array of cells
+    bool mdok = true;                   // Status of MD lookup
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // Cell of interest
+        psRegion *trimsec = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.TRIMSEC"); // Trim section
+        if (!mdok || !trimsec || psRegionIsNaN(*trimsec)) {
+            psError(PS_ERR_UNKNOWN, true, "CELL.TRIMSEC hasn't been set for cell %d.\n", i);
+            return false;
+        }
+
+        if (REGIONS_OVERLAP(trimsec, bounds)) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+// Make sure the BIASSEC doesn't overlap with the established image bounds
+static bool chipContiguousBiassec(psRegion *bounds, // The bounds of the image, altered if primary==true
+                                  const pmChip *chip // The chip to examine for contiguity
+                                 )
+{
+    assert(bounds);
+    assert(chip);
+
+    // Check that the biases don't get in the way
+    psArray *cells = chip->cells;       // The array of cells
+    bool mdok = true;                   // Status of MD lookup
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // Cell of interest
+        psList *biassecs = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.BIASSEC"); // Bias sections
+        if (!mdok || !biassecs) {
+            psError(PS_ERR_UNKNOWN, true, "CELL.BIASSEC hasn't been set for cell %d.\n", i);
+            return false;
+        }
+        if (biassecs->n == 0) {
+            // No point allocating an iterator if there's nothing there to iterate on
+            continue;
+        }
+        psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false); // Iterator
+        psRegion *biassec = NULL;       // Bias section from iteration
+        while ((biassec = psListGetAndIncrement(biassecsIter))) {
+            if (psRegionIsNaN(*biassec)) {
+                continue;
+            }
+            if (REGIONS_OVERLAP(biassec, bounds)) {
+                psFree(biassecsIter);
+                return false;
+            }
+        }
+        psFree(biassecsIter);
+    }
+
+    // If we've gotten this far, everything is fine.
+    return true;
+}
+
+// Are the pixels for the FPA contiguous on the HDU?
+// Work this out by examining all the CELL.TRIMSEC and CELL.BIASSEC regions for the component cells
+static bool fpaContiguous(psRegion *bounds, // The bounds of the image, returned
+                          const pmFPA *fpa // The FPA to examine for contiguity
+                         )
+{
+    assert(bounds);
+    assert(fpa);
+
+    *bounds = psRegionSet(INFINITY, 0, INFINITY, 0);
+
+    // Get the size of the pixels on the HDU
+    psArray *chips = fpa->chips;        // The array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        if (!chipBounds(bounds, chip)) {
+            return false;
+        }
+    }
+
+    // Make sure the bias regions don't get in the way of the HDU
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        if (!chipContiguousBiassec(bounds, chip)) {
+            return false;
+        }
+    }
+
+    // If we got through it all, they must all be contiguous
+    return true;
+}
+
+
+
+// Check a cell for niceness in the parity and binning
+static bool niceCellParityBinning(int *xBin, int *yBin, // Binning for cell, to be returned
+                                  const pmCell *cell // Cell to check for niceness
+                                 )
+{
+    assert(xBin);
+    assert(yBin);
+    assert(cell);
+
+    // A "nice" cell must have only a single readout
+    if (cell->readouts->n != 1) {
+        return false;
+    }
+
+    // A "nice" cell must have parity == 1
+    bool mdok = true;                   // Status of MD lookup
+    int xParity = psMetadataLookupS32(&mdok, cell->concepts, "CELL.XPARITY"); // Parity in x
+    if (!mdok || xParity == 0) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.XPARITY hasn't been set for cell.\n");
+        return false;
+    }
+    if (xParity != 1) {
+        return false;
+    }
+    int yParity = psMetadataLookupS32(&mdok, cell->concepts, "CELL.YPARITY"); // Parity in y
+    if (!mdok || yParity == 0) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.YPARITY hasn't been set for cell.\n");
+        return false;
+    }
+    if (yParity != 1) {
+        return false;
+    }
+
+    // A "nice" cell must have consistent binning
+    int xBinCell = psMetadataLookupS32(&mdok, cell->concepts, "CELL.XBIN"); // Binning in x
+    if (!mdok || xBin <= 0) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.XBIN hasn't been set for cell.\n");
+        return false;
+    }
+    int yBinCell = psMetadataLookupS32(&mdok, cell->concepts, "CELL.YBIN"); // Binning in y
+    if (!mdok || yBin <= 0) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.YBIN hasn't been set for cell.\n");
+        return false;
+    }
+    if (*xBin == 0 || *yBin == 0) {
+        *xBin = xBinCell;
+        *yBin = yBinCell;
+    } else if (xBinCell != *xBin || yBinCell != *yBin) {
+        return false;
+    }
+
+    return true;
+}
+
+
+// Check a cell for niceness in the boundaries
+static bool niceCellBounds(const pmCell *cell, // Cell to check for niceness
+                           const psRegion *imageBounds // Bounds of the image on the HDU
+                          )
+{
+    // A "nice" cell must have the (0,0) pixel at CELL.X0,CELL.Y0
+    bool mdok = true;                   // Status of MD lookup
+    int x0 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.X0"); // Position of (0,0) on chip
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.X0 hasn't been set for cell.\n");
+        return false;
+    }
+    int y0 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.Y0"); // Position of (0,0) on chip
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.Y0 hasn't been set for cell.\n");
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[0]; // A representative readout
+    if (!readout) {
+        return false;                   // Nothing here
+    }
+    if (x0 != readout->col0 + readout->image->col0 - (int)imageBounds->x0 ||
+            y0 != readout->row0 + readout->image->row0 - (int)imageBounds->y0) {
+        psTrace("psModules.camera", 5, "CELL.X0,Y0 don't match: %d,%d vs %d,%d\n", x0, y0,
+                readout->col0 + readout->image->col0 - (int)imageBounds->x0,
+                readout->row0 + readout->image->row0 - (int)imageBounds->y0);
+        return false;
+    }
+
+    return true;
+}
+
+
+// Is the chip "nice"?  If so, return the region containing the chip pixels
+static psRegion *niceChip(int *xBinChip, int *yBinChip, // Binning for chip, to be returned
+                          const pmChip *chip // Chip to examine for "niceness".
+                         )
+{
+    assert(xBinChip);
+    assert(yBinChip);
+    assert(chip);
+
+    // Check that we've got the HDU in the chip or the FPA
+    if ((!chip->hdu || !chip->hdu->images) && (!chip->parent->hdu || !chip->parent->hdu->images)) {
+        return NULL;
+    }
+
+    // Check parity and binning for component cells
+    *xBinChip = 0;
+    *yBinChip = 0;
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i]; // The cell of interest
+        if (!niceCellParityBinning(xBinChip, yBinChip, cell)) {
+            return NULL;
+        }
+    }
+
+    // Now check that the pixels are all contiguous
+    psRegion *imageBounds = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Bound of image on HDU
+    if (!chipBounds(imageBounds, chip) || !chipContiguousBiassec(imageBounds, chip)) {
+        psTrace("psModules.camera", 5, "Image isn't contiguous.\n");
+        psFree(imageBounds);
+        return NULL;
+    }
+
+    psString region = psRegionToString(*imageBounds);
+    psTrace("psModules.camera", 7, "Image bounds: %s\n", region);
+    psFree(region);
+
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i]; // The cell of interest
+        if (!niceCellBounds(cell, imageBounds)) {
+            psFree(imageBounds);
+            return NULL;
+        }
+    }
+
+    // Need to check all the other chips if the HDU is in the FPA
+    pmFPA *fpa = chip->parent;          // The parent FPA
+    if (fpa->hdu && fpa->hdu->images) {
+        psArray *chips = fpa->chips;    // Array of chips
+        for (int i = 0; i < chips->n; i++) {
+            pmChip *testChip = chips->data[i]; // The chip of interest
+            if (testChip == chip) {
+                // Already done this one
+                continue;
+            }
+            if (!chipContiguousTrimsec(imageBounds, testChip) ||
+                    !chipContiguousBiassec(imageBounds, testChip)) {
+                psTrace("psModules.camera", 5, "Image isn't contiguous.\n");
+                psFree(imageBounds);
+                return NULL;
+            }
+        }
+    }
+
+    return imageBounds;
+}
+
+// Is the FPA "nice"?  If so, return the region containing the FPA pixels
+static psRegion *niceFPA(int *xBinFPA, int *yBinFPA, // Binning for FPA, to be returned
+                         const pmFPA *fpa  // FPA to examine for "niceness".
+                        )
+{
+    assert(xBinFPA);
+    assert(yBinFPA);
+    assert(fpa);
+
+    // Check that we've got the HDU in the chip or the FPA
+    if (!fpa->hdu || !fpa->hdu->images) {
+        return NULL;
+    }
+
+    // Check parity and binning for component cells
+    *xBinFPA = 0;
+    *yBinFPA = 0;
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i]; // The chip of interest
+        for (int j = 0; i < chip->cells->n; i++) {
+            pmCell *cell = chip->cells->data[j]; // The cell of interest
+            if (!niceCellParityBinning(xBinFPA, yBinFPA, cell)) {
+                return NULL;
+            }
+        }
+    }
+
+    // Now check that the pixels are all contiguous
+    psRegion *imageBounds = psRegionAlloc(0, 0, 0, 0); // Bound of image on HDU
+    if (!fpaContiguous(imageBounds, fpa)) {
+        psTrace("psModules.camera", 5, "Image isn't contiguous.\n");
+        psFree(imageBounds);
+        return NULL;
+    }
+
+    psString region = psRegionToString(*imageBounds);
+    psTrace("psModules.camera", 7, "Image bounds: %s\n", region);
+    psFree(region);
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i]; // The chip of interest
+        for (int j = 0; i < chip->cells->n; i++) {
+            pmCell *cell = chip->cells->data[j]; // The cell of interest
+            if (!niceCellBounds(cell, imageBounds)) {
+                psFree(imageBounds);
+                return NULL;
+            }
+        }
+    }
+
+    return imageBounds;
+}
+
+// supporting macros used by imageMosaic()
+// copy pixels without binning
+#define COPY_WITH_PARITY_DIFFERENCE(TYPE) \
+        case PS_TYPE_##TYPE: { \
+                for (int y = 0; y < image->numRows; y++) { \
+                    int yTarget =  yTargetBase + yParity * y; \
+                    for (int x = 0; x < image->numCols; x++) { \
+                        int xTarget = xTargetBase + xParity * x; \
+                        mosaic->data.TYPE[yTarget][xTarget] = image->data.TYPE[y][x]; \
+                    } \
+                } \
+            } \
+            break;
+
+// In case the original image is binned but the mosaic is not, we need to fill in the values in
+// the mosaic.  this operation should be replaced with a call to one of the functions defined
+// in psImageBinning
+#define FILL_IN(TYPE) \
+        case PS_TYPE_##TYPE: \
+            for (int y = 0; y < image->numRows; y++) { \
+                float yTargetBinBase = yTargetBase + yParity * yBinSource->data.S32[i] * y / yBinTarget; \
+                for (int x = 0; x < image->numCols; x++) { \
+                    float xTargetBinBase = xTargetBase + xParity * xBinSource->data.S32[i] * x / xBinTarget; \
+                    for (int j = 0; j < yBinSource->data.S32[i]; j++) { \
+                        int yTarget = (int)(yTargetBinBase + yParity * (float)j / (float)yBinTarget); \
+                        for (int k = 0; k < xBinSource->data.S32[i]; k++) { \
+                            int xTarget = (int)(xTargetBinBase + xParity * (float)k / (float)xBinTarget); \
+                            mosaic->data.TYPE[yTarget][xTarget] = image->data.TYPE[y][x]; \
+                        } \
+                    } \
+                } \
+            } \
+            break;
+
+// Mosaic multiple images, with flips, binning and offsets
+static psImage *imageMosaic(const psArray *source, // Images to splice in
+                            const psVector *xFlip, const psVector *yFlip, // Need to flip x and y?
+                            const psVector *xBinSource, // Binning in x of source images
+                            const psVector *yBinSource, // Binning in y of source images
+                            int xBinTarget, int yBinTarget, // Binning in x and y of target images
+                            const psVector *x0, const psVector *y0, // Offsets for source images on target
+                            double unexposed // Value for unexposed pixels
+                           )
+{
+    assert(source);
+    assert(xFlip && xFlip->type.type == PS_TYPE_U8);
+    assert(yFlip && yFlip->type.type == PS_TYPE_U8);
+    assert(xBinSource && xBinSource->type.type == PS_TYPE_S32);
+    assert(yBinSource && yBinSource->type.type == PS_TYPE_S32);
+    assert(x0 && x0->type.type == PS_TYPE_S32);
+    assert(y0 && y0->type.type == PS_TYPE_S32);
+    assert(xFlip->n == source->n);
+    assert(yFlip->n == source->n);
+    assert(xBinSource->n == source->n);
+    assert(yBinSource->n == source->n);
+    assert(x0->n == source->n);
+    assert(y0->n == source->n);
+
+    if (source->n == 0) {
+        return NULL;
+    }
+
+    // Get the maximum extent of the mosaic image
+    int xMin = +INT_MAX;
+    int xMax = -INT_MAX;
+    int yMin = +INT_MAX;
+    int yMax = -INT_MAX;
+    psElemType type = 0;
+    int numImages = 0;                  // Number of images
+    psTrace("psModules.camera", 3, "Mosaicking %ld cells.\n", source->n);
+    for (int i = 0; i < source->n; i++) {
+        psImage *image = source->data[i]; // The image of interest
+        if (!image) {
+            continue;
+        }
+        numImages++;
+
+        // Only implemented for F32 and U8 images so far.
+        assert(image->type.type == PS_TYPE_F32 || image->type.type == PS_TYPE_U8);
+        // All input types must be the same
+        if (type == 0) {
+            type = image->type.type;
+        }
+        assert(type == image->type.type);
+
+        // Size of cell in x and y
+        int xParity = xFlip->data.U8[i] ? -1 : 1;
+        int yParity = yFlip->data.U8[i] ? -1 : 1;
+        psTrace("psModules.camera", 5, "Extent of cell %d: %d -> %d , %d -> %d\n", i, x0->data.S32[i],
+                x0->data.S32[i] + xParity * xBinSource->data.S32[i] * image->numCols, y0->data.S32[i],
+                y0->data.S32[i] + yParity * yBinSource->data.S32[i] * image->numRows);
+
+        COMPARE(x0->data.S32[i], xMin, xMax);
+        COMPARE(y0->data.S32[i], yMin, yMax);
+        // Subtract the parity to get the inclusive limit (not exclusive)
+        COMPARE(x0->data.S32[i] + xParity * xBinSource->data.S32[i] * image->numCols - xParity, xMin, xMax);
+        COMPARE(y0->data.S32[i] + yParity * yBinSource->data.S32[i] * image->numRows - yParity, yMin, yMax);
+    }
+    if (numImages == 0) {
+        return NULL;
+    }
+
+    // Set up the image
+    // Since both upper and lower values are inclusive, we need to add one to the size
+    float xSize = (float)(xMax - xMin + 1) / (float)xBinTarget;
+    if (xSize - (int)xSize > 0) {
+        xSize += 1;
+    }
+    float ySize = (float)(yMax - yMin + 1) / (float)yBinTarget;
+    if (ySize - (int)ySize > 0) {
+        ySize += 1;
+    }
+
+    psTrace("psModules.camera", 3, "Spliced image will be %dx%d\n", (int)xSize, (int)ySize);
+    psImage *mosaic = psImageAlloc((int)xSize, (int)ySize, type); // The mosaic image
+    psImageInit(mosaic, unexposed);
+
+    // Next pass through the images to do the mosaicking
+    // XXX this function uses summing for the output: is this the right choice?
+    for (int i = 0; i < source->n; i++) {
+        psImage *image = source->data[i]; // The image of interest
+        if (!image) {
+            continue;
+        }
+        int xParity = xFlip->data.U8[i] ? -1 : 1; // Parity difference, in x
+        int yParity = yFlip->data.U8[i] ? -1 : 1; // Parity difference, in y
+        int xTargetBase = (x0->data.S32[i] - xMin) / xBinTarget; // The base x position in the target frame
+        int yTargetBase = (y0->data.S32[i] - yMin) / yBinTarget; // The base y position in the target frame
+
+        // in the first case, we are just copy a section pixel-by-pixel
+        if ((xBinSource->data.S32[i] == xBinTarget) &&
+            (yBinSource->data.S32[i] == yBinTarget) &&
+            (xFlip->data.U8[i] == 0) &&
+            (yFlip->data.U8[i] == 0)) {
+            // Let someone else do the hard work
+            psImageOverlaySection(mosaic, image, xTargetBase, yTargetBase, "=");
+            continue;
+        }
+
+        // in the second case, there's a difference with the parities, but we don't have to
+        // worry about binning
+        if (xBinSource->data.S32[i] == xBinTarget && yBinSource->data.S32[i] == yBinTarget) {
+            switch (type) {
+                COPY_WITH_PARITY_DIFFERENCE(F32);
+                COPY_WITH_PARITY_DIFFERENCE(U8);
+              default:
+                psAbort("Should never get here.\n");
+            }
+            continue;
+        }
+
+        // In the third case, the images are flipped and have different binnnig.
+        // We have to do all of the hard work ourselves
+        switch (type) {
+            FILL_IN(F32);
+            FILL_IN(U8);
+          default:
+            psAbort("Should never get here.\n");
+        }
+    } // Iterating over images
+
+    return mosaic;
+}
+
+// Add a cell and its various properties to the arrays
+static bool addCell(psArray *images,    // Array of images
+                    psArray *masks,     // Array of masks
+                    psArray *weights,   // Array of weights
+                    psVector *x0,       // Array of X0
+                    psVector *y0,       // Array of Y0
+                    psVector *xBin,     // Array of XBIN
+                    psVector *yBin,     // Array of YBIN
+                    psVector *xFlip,    // Array indicating whether x axis should be flipped
+                    psVector *yFlip,    // Array indicating whether y axis should be flipped
+                    const pmCell *cell, // Cell to add
+                    int *xBinMin,       // The minimum x binning, returned
+                    int *yBinMin,       // The minimum y binning, returned
+                    bool chipStuff,      // Worry about chip stuff as well?
+                    int x0Target, int y0Target, // Target x0 and y0 offsets
+                    int xParityTarget, int yParityTarget // Target parities
+                   )
+{
+    if (!cell) {
+        return false;
+    }
+
+    // Expand the arrays and vectors to handle new data
+    long index = images->n;               // The index to use
+    if (images->n == images->nalloc) {
+        images  = psArrayRealloc(images,  index + CELL_LIST_BUFFER);
+        masks   = psArrayRealloc(masks,   index + CELL_LIST_BUFFER);
+        weights = psArrayRealloc(weights, index + CELL_LIST_BUFFER);
+        x0    = psVectorRealloc(x0,    index+ CELL_LIST_BUFFER);
+        y0    = psVectorRealloc(y0,    index+ CELL_LIST_BUFFER);
+        xBin  = psVectorRealloc(xBin,  index+ CELL_LIST_BUFFER);
+        yBin  = psVectorRealloc(yBin,  index+ CELL_LIST_BUFFER);
+        xFlip = psVectorRealloc(xFlip, index+ CELL_LIST_BUFFER);
+        yFlip = psVectorRealloc(yFlip, index+ CELL_LIST_BUFFER);
+    }
+
+    images->n = index + 1;
+    masks->n = index + 1;
+    weights->n = index + 1;
+    x0->n = index + 1;
+    y0->n = index + 1;
+    xBin->n = index + 1;
+    yBin->n = index + 1;
+    xFlip->n = index + 1;
+    yFlip->n = index + 1;
+
+    bool mdok = true;                   // Status of MD lookup
+    bool good = true;                   // Is everything good?
+
+    // Offset of the cell on the chip
+    int x0Cell = psMetadataLookupS32(&mdok, cell->concepts, "CELL.X0");
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.X0 for cell is not set.\n");
+        good = false;
+    }
+    int y0Cell = psMetadataLookupS32(&mdok, cell->concepts, "CELL.Y0");
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.Y0 for cell is not set.\n");
+        good = false;
+    }
+    psTrace("psModules.camera", 5, "Cell %ld: x0=%d y0=%d\n", index, x0Cell, y0Cell);
+
+    // Offset of the chip on the FPA
+    int x0Chip = 0, y0Chip = 0;
+    if (chipStuff) {
+        pmChip *chip = cell->parent;    // The parent chip
+        if (!chip) {
+            psError(PS_ERR_UNKNOWN, true, "Cell has no parent chip --- can't find CHIP.X0 and CHIP.Y0\n");
+            good = false;
+        }
+        x0Chip = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.X0");
+        if (!mdok) {
+            psError(PS_ERR_UNKNOWN, true, "CHIP.X0 for chip is not set.\n");
+            good = false;
+        }
+        y0Chip = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.Y0");
+        if (!mdok) {
+            psError(PS_ERR_UNKNOWN, true, "CHIP.Y0 for chip is not set.\n");
+            good = false;
+        }
+    }
+
+    // Binning
+    xBin->data.S32[index] = psMetadataLookupS32(&mdok, cell->concepts, "CELL.XBIN");
+    if (!mdok || xBin->data.S32[index] == 0) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.XBIN for cell is not set.\n");
+        return false;
+    } else if (xBin->data.S32[index] < *xBinMin) {
+        *xBinMin = xBin->data.S32[index];
+    }
+    yBin->data.S32[index] = psMetadataLookupS32(&mdok, cell->concepts, "CELL.YBIN");
+    if (!mdok || yBin->data.S32[index] == 0) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.YBIN for cell is not set.\n");
+        return false;
+    } else if (yBin->data.S32[index] < *yBinMin) {
+        *yBinMin = yBin->data.S32[index];
+    }
+
+    // Do we need to flip?
+    int xParityCell = psMetadataLookupS32(&mdok, cell->concepts, "CELL.XPARITY");
+    if (!mdok || (xParityCell != 1 && xParityCell != -1)) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.XPARITY for cell is not set.\n");
+        return false;
+    }
+    int yParityCell = psMetadataLookupS32(&mdok, cell->concepts, "CELL.YPARITY");
+    if (!mdok || (yParityCell != 1 && yParityCell != -1)) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.YPARITY for cell is not set.\n");
+        return false;
+    }
+
+    // Parity of the chip on the FPA
+    int xParityChip = 1, yParityChip = 1;
+    if (chipStuff) {
+        pmChip *chip = cell->parent;    // The parent chip
+        xParityChip = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.XPARITY");
+        if (!mdok || (xParityChip != 1 && xParityChip != -1)) {
+            psError(PS_ERR_UNKNOWN, true, "CHIP.XPARITY for chip is not set.\n");
+            return false;
+        }
+        yParityChip = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.YPARITY");
+        if (!mdok || (yParityChip != 1 && yParityChip != -1)) {
+            psError(PS_ERR_UNKNOWN, true, "CHIP.YPARITY for chip is not set.\n");
+            return false;
+        }
+    }
+
+    // Set the flips on the basis of the parity
+    // XXX if (level == CHIP) : only apply Cell parity
+    // XXX if (level == FPA) : apply Chip & Cell parity
+    if (xParityCell * xParityChip == xParityTarget) {
+        xFlip->data.U8[index] = 0;
+    } else {
+        xFlip->data.U8[index] = 1;
+    }
+    if (yParityCell * yParityChip == yParityTarget) {
+        yFlip->data.U8[index] = 0;
+    } else {
+        yFlip->data.U8[index] = 1;
+    }
+
+    x0->data.S32[index] = x0Chip + x0Cell - x0Target;
+    y0->data.S32[index] = y0Chip + y0Cell - y0Target;
+
+    // Add the readout to the array of images to be mosaicked
+    psArray *readouts = cell->readouts; // The array of readouts
+    if (readouts->n > 1) {
+        psLogMsg(__func__, PS_LOG_WARN, "Cell contains more than one readout (%ld) --- only the first will "
+                 "be mosaicked.\n", readouts->n);
+    }
+    pmReadout *readout = readouts->data[0]; // The only readout we'll bother with
+
+    // The images to put into the mosaic
+    images->data[index]  = psMemIncrRefCounter(readout->image);
+    weights->data[index] = psMemIncrRefCounter(readout->weight);
+    masks->data[index]   = psMemIncrRefCounter(readout->mask);
+
+    psTrace("psModules.camera", 9, "Added cell (%p) %ld: %d,%d; %d,%d, %d,%d.\n", cell, index,
+            x0->data.S32[index], y0->data.S32[index], xBin->data.S32[index], yBin->data.S32[index],
+            xFlip->data.U8[index], yFlip->data.U8[index]);
+
+    return true;
+}
+
+
+// Mosaic together the cells in a chip
+static bool chipMosaic(psImage **mosaicImage, // The mosaic image, to be returned
+                       psImage **mosaicMask, // The mosaic mask, to be returned
+                       psImage **mosaicWeights, // The mosaic weights, to be returned
+                       int *xBinChip, int *yBinChip, // The binning in x and y, to be returned
+                       const pmChip *chip, // Chip to mosaic
+                       const pmCell *targetCell, // Cell to which to mosaic
+                       psMaskType blank // Mask value to give blank pixels
+                      )
+{
+    assert(mosaicImage);
+    assert(mosaicMask);
+    assert(mosaicWeights);
+    assert(xBinChip);
+    assert(yBinChip);
+    assert(chip);
+    assert(targetCell);
+
+    psArray *images = psArrayAlloc(0); // Array of images that will be mosaicked
+    psArray *weights = psArrayAlloc(0); // Array of weight images to be mosaicked
+    psArray *masks = psArrayAlloc(0); // Array of mask images to be mosaicked
+    psVector *x0 = psVectorAlloc(0, PS_TYPE_S32); // Origin x coordinates
+    psVector *y0 = psVectorAlloc(0, PS_TYPE_S32); // Origin y coordinates
+    psVector *xBin = psVectorAlloc(0, PS_TYPE_S32); // Binning in x
+    psVector *yBin = psVectorAlloc(0, PS_TYPE_S32); // Binning in y
+    psVector *xFlip = psVectorAlloc(0, PS_TYPE_U8); // Flip in x?
+    psVector *yFlip = psVectorAlloc(0, PS_TYPE_U8); // Flip in y?
+
+    // Get the target characteristics
+    bool mdok = true;                   // Status of MD lookup
+    int x0Target = psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.X0");
+    if (!mdok) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.X0 is not set for the target cell; assuming 0.\n");
+        FIX_CONCEPT(targetCell->concepts, "CELL.X0", S32, 0);
+    }
+    int y0Target = psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.Y0");
+    if (!mdok) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.Y0 is not set for the target cell; assuming 0.\n");
+        FIX_CONCEPT(targetCell->concepts, "CELL.Y0", S32, 0);
+    }
+    int xParityTarget = psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.XPARITY");
+    if (!mdok || (xParityTarget != -1 && xParityTarget != 1)) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.XPARITY is not set for the target cell; assuming 1.\n");
+        FIX_CONCEPT(targetCell->concepts, "CELL.XPARITY", S32, 1);
+        xParityTarget = 1;
+    }
+    int yParityTarget = psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.YPARITY");
+    if (!mdok || (yParityTarget != -1 && yParityTarget != 1)) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.YPARITY is not set for the target cell; assuming 1.\n");
+        FIX_CONCEPT(targetCell->concepts, "CELL.YPARITY", S32, 1);
+        yParityTarget = 1;
+    }
+
+    // Binning for the mosaicked chip is the minimum binning allowed by the cells
+    *xBinChip = INT_MAX;
+    *yBinChip = INT_MAX;
+
+    // Set up the required inputs
+    bool allGood = true;                // Is everything good, well-behaved?
+    psArray *cells = chip->cells;       // The array of cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // The cell of interest
+        if (!cell || !cell->data_exists) {
+            continue;
+        }
+        allGood &= addCell(images, masks, weights, x0, y0, xBin, yBin, xFlip, yFlip,
+                           cell, xBinChip, yBinChip, false, x0Target, y0Target,
+                           xParityTarget, yParityTarget);
+    }
+
+    // Check to see if the target has a smaller binning in mind
+    int xBinTarget = psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.XBIN");
+    if (!mdok || xBinTarget == 0) {
+        // CELL.XBIN is not set for the target cell --- assume it's the same as the source
+        FIX_CONCEPT(targetCell->concepts, "CELL.XBIN", S32, *xBinChip);
+    } else {
+        *xBinChip = xBinTarget;
+    }
+    int yBinTarget = psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.YBIN");
+    if (!mdok || yBinTarget == 0) {
+        // CELL.YBIN is not set for the target cell --- assume it's the same as the source
+        FIX_CONCEPT(targetCell->concepts, "CELL.YBIN", S32, *yBinChip);
+    } else {
+        *yBinChip = yBinTarget;
+    }
+
+    // Mosaic the images together and we're done
+    if (allGood) {
+        *mosaicImage = imageMosaic(images, xFlip, yFlip, xBin, yBin, *xBinChip, *yBinChip, x0, y0, BLANK_VALUE);
+        *mosaicWeights = imageMosaic(weights, xFlip, yFlip, xBin, yBin, *xBinChip, *yBinChip, x0, y0, BLANK_VALUE);
+        *mosaicMask = imageMosaic(masks, xFlip, yFlip, xBin, yBin, *xBinChip, *yBinChip, x0, y0, blank);
+    }
+
+    // Clean up
+    psFree(images);
+    psFree(weights);
+    psFree(masks);
+    psFree(xFlip);
+    psFree(yFlip);
+    psFree(xBin);
+    psFree(yBin);
+    psFree(x0);
+    psFree(y0);
+
+    return allGood;
+}
+
+// Mosaic together the cells in a FPA
+static bool fpaMosaic(psImage **mosaicImage, // The mosaic image, to be returned
+                      psImage **mosaicMask, // The mosaic mask, to be returned
+                      psImage **mosaicWeights, // The mosaic weights, to be returned
+                      int *xBinFPA, int *yBinFPA, // The binning in x and y, to be returned
+                      const pmFPA *fpa,  // FPA to mosaic
+                      const pmChip *targetChip, // Chip to which to mosaic
+                      const pmCell *targetCell, // Cell to which to mosaic
+                      psMaskType blank  // Mask value to give blank pixels
+                     )
+{
+    assert(mosaicImage);
+    assert(mosaicMask);
+    assert(mosaicWeights);
+    assert(xBinFPA);
+    assert(yBinFPA);
+    assert(fpa);
+    assert(targetChip);
+    assert(targetCell);
+
+    psArray *images = psArrayAlloc(0); // Array of images that will be mosaicked
+    psArray *weights = psArrayAlloc(0); // Array of weight images to be mosaicked
+    psArray *masks = psArrayAlloc(0); // Array of mask images to be mosaicked
+    psVector *x0 = psVectorAlloc(0, PS_TYPE_S32); // Origin x coordinates
+    psVector *y0 = psVectorAlloc(0, PS_TYPE_S32); // Origin y coordinates
+    psVector *xBin = psVectorAlloc(0, PS_TYPE_S32); // Binning in x
+    psVector *yBin = psVectorAlloc(0, PS_TYPE_S32); // Binning in y
+    psVector *xFlip = psVectorAlloc(0, PS_TYPE_U8); // Flip in x?
+    psVector *yFlip = psVectorAlloc(0, PS_TYPE_U8); // Flip in y?
+
+    // Get the target characteristics
+    bool mdok = true;                   // Status of MD lookup
+    int x0Target = psMetadataLookupS32(&mdok, targetChip->concepts, "CHIP.X0");
+    if (!mdok) {
+        psLogMsg(__func__, PS_LOG_WARN, "CHIP.X0 is not set for the target chip; assuming 0.\n");
+        FIX_CONCEPT(targetChip->concepts, "CHIP.X0", S32, 0);
+    }
+    int y0Target = psMetadataLookupS32(&mdok, targetChip->concepts, "CHIP.Y0");
+    if (!mdok) {
+        psLogMsg(__func__, PS_LOG_WARN, "CHIP.Y0 is not set for the target chip; assuming 0.\n");
+        FIX_CONCEPT(targetChip->concepts, "CHIP.Y0", S32, 0);
+    }
+    x0Target += psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.X0");
+    if (!mdok) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.X0 is not set for the target cell; assuming 0.\n");
+        FIX_CONCEPT(targetCell->concepts, "CELL.X0", S32, 0);
+    }
+    y0Target += psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.Y0");
+    if (!mdok) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.Y0 is not set for the target cell; assuming 0.\n");
+        FIX_CONCEPT(targetCell->concepts, "CELL.Y0", S32, 0);
+    }
+    int xParityChipTarget = psMetadataLookupS32(&mdok, targetChip->concepts, "CHIP.XPARITY");
+    if (!mdok || (xParityChipTarget != -1 && xParityChipTarget != 1)) {
+        psLogMsg(__func__, PS_LOG_WARN, "CHIP.XPARITY is not set for the target chip; assuming 1.\n");
+        FIX_CONCEPT(targetChip->concepts, "CHIP.XPARITY", S32, 1);
+        xParityChipTarget = 1;
+    }
+    int yParityChipTarget = psMetadataLookupS32(&mdok, targetChip->concepts, "CHIP.YPARITY");
+    if (!mdok || (yParityChipTarget != -1 && yParityChipTarget != 1)) {
+        psLogMsg(__func__, PS_LOG_WARN, "CHIP.YPARITY is not set for the target chip; assuming 1.\n");
+        FIX_CONCEPT(targetChip->concepts, "CHIP.YPARITY", S32, 1);
+        yParityChipTarget = 1;
+    }
+    int xParityCellTarget = psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.XPARITY");
+    if (!mdok || (xParityCellTarget != -1 && xParityCellTarget != 1)) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.XPARITY is not set for the target cell; assuming 1.\n");
+        FIX_CONCEPT(targetCell->concepts, "CELL.XPARITY", S32, 1);
+        xParityCellTarget = 1;
+    }
+    int yParityCellTarget = psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.YPARITY");
+    if (!mdok || (yParityCellTarget != -1 && yParityCellTarget != 1)) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.YPARITY is not set for the target cell; assuming 1.\n");
+        FIX_CONCEPT(targetCell->concepts, "CELL.YPARITY", S32, 1);
+        yParityCellTarget = 1;
+    }
+    int xParityTarget = xParityChipTarget * xParityCellTarget;
+    int yParityTarget = yParityChipTarget * yParityCellTarget;
+
+    // Binning for the mosaicked chip is the minimum binning allowed by the cells
+    *xBinFPA = INT_MAX;
+    *yBinFPA = INT_MAX;
+
+    // Set up the required inputs
+    bool allGood = true;                // Is everything good, well-behaved?
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // The chip of interest
+        if (!chip || !chip->data_exists) {
+            continue;
+        }
+        psArray *cells = chip->cells;   // The array of cells
+        for (int j = 0; j < cells->n; j++) {
+            pmCell *cell = cells->data[j];  // The cell of interest
+            if (!cell || !cell->data_exists) {
+                continue;
+            }
+            allGood |= addCell(images, masks, weights, x0, y0, xBin, yBin, xFlip, yFlip,
+                               cell, xBinFPA, yBinFPA, true, x0Target, y0Target,
+                               xParityTarget, yParityTarget);
+        }
+    }
+
+    // Check to see if the target has a smaller binning in mind
+    int xBinTarget = psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.XBIN");
+    if (mdok && xBinTarget != 0) {
+        *xBinFPA = xBinTarget;
+    }
+    int yBinTarget = psMetadataLookupS32(&mdok, targetCell->concepts, "CELL.YBIN");
+    if (mdok && yBinTarget != 0) {
+        *yBinFPA = yBinTarget;
+    }
+
+    // Mosaic the images together and we're done
+    if (allGood) {
+        *mosaicImage = imageMosaic(images, xFlip, yFlip, xBin, yBin, *xBinFPA, *yBinFPA, x0, y0, BLANK_VALUE);
+        *mosaicWeights = imageMosaic(weights, xFlip, yFlip, xBin, yBin, *xBinFPA, *yBinFPA, x0, y0, BLANK_VALUE);
+        *mosaicMask = imageMosaic(masks, xFlip, yFlip, xBin, yBin, *xBinFPA, *yBinFPA, x0, y0, blank);
+    }
+
+    // Clean up
+    psFree(images);
+    psFree(weights);
+    psFree(masks);
+    psFree(xFlip);
+    psFree(yFlip);
+    psFree(xBin);
+    psFree(yBin);
+    psFree(x0);
+    psFree(y0);
+
+    return allGood;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Mosaic all the cells in a chip together.
+//
+// It is desirable to do this without using psImageOverlay (or similar) if it can be at all avoided (because
+// it's really really slow in that case).  There are therefore two cases:
+//
+// 1. The HDU is at the Chip or FPA level.  This is the fast case, and only works if the HDU is "nice", by
+// which I mean:
+//
+//    - the CELL.TRIMSECs are contiguous on the HDU image
+//    - the CELL.PARITYs are identically +1
+//    - the CELL.XBIN and CELL.YBIN are all identical
+//
+// Then we can just use psImageSubset to get the "mosaicked" chip.
+//
+//
+// 2. The HDU is at the cell level, or the above requirements are not met, in which case we mosaic the cells.
+// This is the slow case.  We need to:
+//
+//    - Throw away the bias regions
+//    - Convert all cells to common parity
+//    - Mosaic the cells into an HDU image using CELL.X0 and CELL.Y0
+//    - Update CELL.TRIMSECs
+//
+// Once the demands of case 1 have been met, or case 2 has been performed, then we can create a cell to hold
+// the mosaic image.
+
+bool pmChipMosaic(pmChip *target, const pmChip *source, bool deepCopy, psMaskType blank)
+{
+    // Target exists, and has only a single cell
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(target->cells, false);
+    if (target->cells->n != 1) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true, "Target chip for mosaicking must contain a single cell.\n");
+        return false;
+    }
+    pmCell *targetCell = target->cells->data[0]; // The target cell
+    PS_ASSERT_PTR_NON_NULL(targetCell, false);
+    // Source exists
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+
+    psImage *mosaicImage   = NULL;      // The mosaic image
+    psImage *mosaicMask    = NULL;      // The mosaic mask
+    psImage *mosaicWeights = NULL;      // The mosaic weights
+
+    // Find the HDU
+    psRegion *chipRegion = NULL;        // Region on the HDU that corresponds to the chip
+    int xBin = 0, yBin = 0;             // Binning for the chip mosaic
+    if (!deepCopy && (chipRegion = niceChip(&xBin, &yBin, source))) {
+        // Case 1 --- we need only cut out the region
+        psTrace("psModules.camera", 1, "Case 1 mosaicking: simple cut-out.\n");
+        pmHDU *hdu = source->hdu;       // The HDU that has the pixels
+        if (!hdu || !hdu->images) {
+            hdu = source->parent->hdu;
+        }
+        // force limits to land on chip
+        psRegion bounds = psRegionForImage (hdu->images->data[0], *chipRegion);
+        mosaicImage = psImageSubset(hdu->images->data[0], bounds);
+        if (!mosaicImage) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to select image pixels.\n");
+            return false;
+        }
+        if (hdu->masks) {
+            mosaicMask = psImageSubset(hdu->masks->data[0], bounds);
+            if (!mosaicMask) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to select mask pixels.\n");
+                return false;
+            }
+        }
+        if (hdu->weights) {
+            mosaicWeights = psImageSubset(hdu->weights->data[0], bounds);
+            if (!mosaicWeights) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to select weight pixels.\n");
+                return false;
+            }
+        }
+    } else {
+        // Case 2 --- we need to mosaic by cut and paste
+        psTrace("psModules.camera", 1, "Case 2 mosaicking: cut and paste.\n");
+        if (!chipMosaic(&mosaicImage, &mosaicMask, &mosaicWeights, &xBin, &yBin, source, targetCell, blank)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to mosaic cells.\n");
+            return false;
+        }
+        chipRegion = psRegionAlloc(0, 0, 0, 0); // We've cut and paste, so there's no valid trimsec
+        *chipRegion = psRegionForImage (mosaicImage, *chipRegion);
+    }
+    psTrace("psModules.camera", 1, "xBin,yBin: %d,%d\n", xBin, yBin);
+
+    // Set the concepts for the target cell
+    psList *sourceCells = psArrayToList(source->cells); // List of cells
+    pmConceptsAverageCells(targetCell, sourceCells, chipRegion, NULL, false);
+    {
+        psMetadataItem *item = psMetadataLookup(targetCell->concepts, "CELL.X0");
+        item->data.S32 = 0;
+        item = psMetadataLookup(targetCell->concepts, "CELL.Y0");
+        item->data.S32 = 0;
+        item = psMetadataLookup(targetCell->concepts, "CELL.XBIN");
+        item->data.S32 = xBin;
+        item = psMetadataLookup(targetCell->concepts, "CELL.YBIN");
+        item->data.S32 = yBin;
+    }
+    psFree(sourceCells);
+    psFree(chipRegion);
+
+    // Copy the concepts
+    target->concepts = psMetadataCopy(target->concepts, source->concepts); // Chip level
+    target->parent->concepts = psMetadataCopy(target->parent->concepts, source->parent->concepts); // FPA lvl
+
+    // Now make a new readout to go in the target cell
+    pmReadout *newReadout = pmReadoutAlloc(targetCell); // New readout
+    newReadout->image  = mosaicImage;
+    newReadout->mask   = mosaicMask;
+    newReadout->weight = mosaicWeights;
+    psFree(newReadout);                 // Drop reference
+
+    // Data now exists in the targets
+    pmChipSetDataStatus(target, true);
+    pmCellSetDataStatus(targetCell, true);
+    newReadout->data_exists = true;
+
+    // Update the headers
+    pmHDU *sourceHDU = pmHDUFromChip(source); // The HDU for the source
+    pmHDU *targetHDU = pmHDUFromChip(target); // The HDU for the target
+    targetHDU->header = psMetadataCopy(targetHDU->header, sourceHDU->header);
+    pmHDU *targetPHU = pmHDUGetHighest(target->parent, target, NULL);
+    pmHDU *sourcePHU = pmHDUGetHighest(source->parent, source, NULL);
+
+    // Need to update NAXIS1, NAXIS2 in the target header, so that when we write a CMF, it has the correct
+    // extent.  I'm not convinced that this is the best way to do this, but it should be, at worst, harmless,
+    // since NAXIS[12] will get overwritten for an image with the proper dimensions.
+    psRegion *naxis = pmChipExtent(target);
+    psMetadataAddS32(targetHDU->header, PS_LIST_TAIL, "NAXIS1", PS_META_REPLACE, "Size in x",
+                     naxis->x1 - naxis->x0);
+    psMetadataAddS32(targetHDU->header, PS_LIST_TAIL, "NAXIS2", PS_META_REPLACE, "Size in y",
+                     naxis->y1 - naxis->y0);
+    psFree(naxis);
+
+
+    if (!targetPHU) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find HDU after mosaicking.\n");
+        return false;
+    }
+    if (!targetPHU->header) {
+        // if we don't yet have a header, copy this one.
+        // XXX do we need to create an empty one if the levels do not match??
+        if (true) {
+            targetPHU->header = psMetadataCopy(targetPHU->header, sourcePHU->header);
+        } else {
+            targetPHU->header = psMetadataAlloc();
+        }
+    }
+
+    if (!pmConfigConformHeader(targetPHU->header, targetPHU->format)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to conform header after mosaicking.\n");
+        return false;
+    }
+
+    // If the cells contain the headers, we need to apply the WCS terms from (one of?) the cells
+    int xParityCellTarget = psMetadataLookupS32(NULL, targetCell->concepts, "CELL.XPARITY");
+    if (xParityCellTarget != -1 && xParityCellTarget != 1) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "CELL.XPARITY is not set for target.");
+        return false;
+    }
+    int xParityChipTarget = psMetadataLookupS32(NULL, target->concepts, "CHIP.XPARITY");
+    if (xParityChipTarget != -1 && xParityChipTarget != 1) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "CHIP.XPARITY is not set for target.");
+        return false;
+    }
+    int xParityTarget = xParityCellTarget * xParityChipTarget; // Target parity in x
+
+    int yParityCellTarget = psMetadataLookupS32(NULL, targetCell->concepts, "CELL.YPARITY");
+    if (yParityCellTarget != -1 && yParityCellTarget != 1) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "CELL.YPARITY is not set for target.");
+        return false;
+    }
+    int yParityChipTarget = psMetadataLookupS32(NULL, target->concepts, "CHIP.YPARITY");
+    if (yParityChipTarget != -1 && yParityChipTarget != 1) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "CHIP.YPARITY is not set for target.");
+        return false;
+    }
+    int yParityTarget = yParityCellTarget * yParityChipTarget; // Target parity in y
+
+    for (int i = 0; i < source->cells->n; i++) {
+        pmCell *cell = source->cells->data[i];
+        if (!cell || !cell->hdu || !cell->hdu->header) {
+            continue;
+        }
+
+        pmAstromWCS *wcs = pmAstromWCSfromHeader(cell->hdu->header); // WCS terms for this cell
+        if (!wcs) {
+            psTrace("psModules.camera", 1, "Unable to read cell WCS to generate chip WCS --- ignored.");
+            continue;
+        }
+
+        int xBinCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN"); // Cell binning in x
+        if (xBinCell == 0) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "CELL.XBIN is not set.");
+            return false;
+        }
+        int xParitySource = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY") *
+            psMetadataLookupS32(NULL, source->concepts, "CHIP.XPARITY"); // Source parity in x
+        if (xParitySource != -1 && xParitySource != 1) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "CHIP.XPARITY or CELL.XPARITY is not set for source.");
+            return false;
+        }
+        bool xFlip = (xParitySource == xParityTarget ? false : true); // Flip the x sense of the WCS?
+        int x0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.X0"); // Cell offset in x
+
+        // Modify the wcs terms for the cell offset, binning, and parity
+        float xBinRatio = (float)xBinCell / (float)xBin;
+        if (xFlip) {
+            wcs->crpix1 = x0Cell - wcs->crpix1 * xBinRatio;
+            wcs->cdelt1 *= -1;
+        } else {
+            wcs->crpix1 = x0Cell + wcs->crpix1 * xBinRatio;
+        }
+        wcs->cdelt1 *= xBinRatio;
+
+        int yBinCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN"); // Cell binning in y
+        if (yBinCell == 0) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "CELL.YBIN is not set.");
+            return false;
+        }
+        int yParitySource = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY") *
+            psMetadataLookupS32(NULL, cell->parent->concepts, "CHIP.YPARITY"); // Source parity in y
+        if (yParitySource != -1 && yParitySource != 1) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "CHIP.YPARITY or CELL.YPARITY is not set for source.");
+            return false;
+        }
+        bool yFlip = (yParitySource == yParityTarget ? false : true); // Flip the y sense of the WCS?
+        int y0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0"); // Cell offset in y
+
+        float yBinRatio = (float)yBinCell / (float)yBin;
+        if (yFlip) {
+            wcs->crpix2 = y0Cell - wcs->crpix2 * yBinRatio;
+            wcs->cdelt2 *= -1;
+        } else {
+            wcs->crpix2 = y0Cell + wcs->crpix2 * yBinRatio;
+        }
+        wcs->cdelt2 *= yBinRatio;
+
+        if (!pmAstromWCStoHeader(targetHDU->header, wcs)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to generate chip WCS from cell WCS.");
+            psFree(wcs);
+            return false;
+        }
+        psFree(wcs);
+
+        // XXX rather than quitting at this point, we could save this wcs structure and compare
+        // its values to the equivalent version from one of the other cells.
+        break;
+    }
+
+    return true;
+}
+
+
+bool pmFPAMosaic(pmFPA *target, const pmFPA *source, bool deepCopy, psMaskType blank)
+{
+    // Target exists, and has only a single chip with single cell
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(target->chips, false);
+    if (target->chips->n != 1) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true, "Target FPA for mosaicking must contain a single chip.\n");
+        return false;
+    }
+    pmChip *targetChip = target->chips->data[0]; // The target chip
+    PS_ASSERT_PTR_NON_NULL(targetChip, false);
+    PS_ASSERT_PTR_NON_NULL(targetChip->cells, false);
+    if (target->chips->n != 1) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true, "Target FPA for mosaicking must contain a single cell.\n");
+        return false;
+    }
+    pmCell *targetCell = targetChip->cells->data[0]; // The target cell
+    PS_ASSERT_PTR_NON_NULL(targetCell, false);
+    // Source exists
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+    psImage *mosaicImage   = NULL;      // The mosaic image
+    psImage *mosaicMask    = NULL;      // The mosaic mask
+    psImage *mosaicWeights = NULL;      // The mosaic weights
+
+    // Find the HDU
+    psRegion *fpaRegion = NULL;         // Region on the HDU that corresponds to the FPA
+    int xBin = 0, yBin = 0;             // Binning for the FPA mosaic
+    if (!deepCopy && (fpaRegion = niceFPA(&xBin, &yBin, source))) {
+        // Case 1 --- we need only cut out the region
+        psTrace("psModules.camera", 1, "Case 1 mosaicking: simple cut-out.\n");
+        pmHDU *hdu = source->hdu;         // The HDU that has the pixels
+        mosaicImage = psImageSubset(hdu->images->data[0], *fpaRegion);
+        if (hdu->masks) {
+            mosaicMask = psImageSubset(hdu->masks->data[0], *fpaRegion);
+        }
+        if (hdu->weights) {
+            mosaicWeights = psImageSubset(hdu->weights->data[0], *fpaRegion);
+        }
+    } else {
+        // Case 2 --- we need to mosaic by cut and paste
+        psTrace("psModules.camera", 1, "Case 2 mosaicking: cut and paste.\n");
+        if (!fpaMosaic(&mosaicImage, &mosaicMask, &mosaicWeights, &xBin, &yBin, source,
+                       targetChip, targetCell, blank)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to mosaic chips.\n");
+            return false;
+        }
+        fpaRegion = psRegionAlloc(NAN, NAN, NAN, NAN); // We've cut and paste, so there's no valid trimsec
+    }
+
+    // Set the concepts for the target cell, and add the mosaic in
+    // First we need a list of cells
+    psList *sourceCells = psListAlloc(NULL); // List of source cells
+    psArray *chips = source->chips;        // Array of chips
+    pmChip *firstSourceChip = NULL;     // The first chip in the source FPA; for headers
+    pmCell *firstSourceCell = NULL;     // The first cell in the source FPA; for headers
+    for (long i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        if (!chip || !chip->data_exists) {
+            continue;
+        }
+        psArray *cells = chip->cells;
+        for (long j = 0; j < cells->n; j++) {
+            pmCell *cell = cells->data[j]; // Cell of interest
+            if (!cell || !cell->data_exists) {
+                continue;
+            }
+            psListAdd(sourceCells, PS_LIST_TAIL, cell);
+
+            // These are valid chip and cell to use for the header; grab the first such
+            if (!firstSourceCell && !firstSourceChip) {
+                firstSourceCell = cell;
+                firstSourceChip = chip;
+            }
+        }
+    }
+    pmConceptsAverageCells(targetCell, sourceCells, fpaRegion, NULL, false);
+    {
+        psMetadataItem *item = psMetadataLookup(targetCell->concepts, "CELL.X0");
+        item->data.S32 = 0;
+        item = psMetadataLookup(targetCell->concepts, "CELL.Y0");
+        item->data.S32 = 0;
+        item = psMetadataLookup(targetCell->concepts, "CELL.XBIN");
+        item->data.S32 = xBin;
+        item = psMetadataLookup(targetCell->concepts, "CELL.YBIN");
+        item->data.S32 = yBin;
+    }
+    psFree(sourceCells);
+    psFree(fpaRegion);
+
+    // Currently, there's nothing interesting in the chip concepts that needs to be updated.
+
+    // Copy the concepts for the target FPA
+    target->concepts = psMetadataCopy(target->concepts, source->concepts);
+
+    // Now make a new readout to go in the new cell
+    pmReadout *newReadout = pmReadoutAlloc(targetCell); // New readout
+    newReadout->image  = mosaicImage;
+    newReadout->mask   = mosaicMask;
+    newReadout->weight = mosaicWeights;
+    psFree(newReadout);                 // Drop reference
+
+    // Data now exists in the targets
+    pmChipSetDataStatus(targetChip, true);
+    pmCellSetDataStatus(targetCell, true);
+    newReadout->data_exists = true;
+
+    // Update the headers
+    pmHDU *sourceHDU = pmHDUGetHighest(source, firstSourceChip, firstSourceCell); // The HDU for the source
+    if (!sourceHDU) {
+        psLogMsg(__func__, PS_LOG_WARN, "Unable to find HDU in source FPA; unable to copy headers.\n");
+        return false;
+    }
+    pmHDU *targetHDU = pmHDUGetHighest(target, targetChip, targetCell); // The HDU for the target
+    if (!targetHDU) {
+        psLogMsg(__func__, PS_LOG_WARN, "Unable to find HDU in target FPA; unable to copy headers.\n");
+        return false;
+    }
+
+    if (sourceHDU->header) {
+        targetHDU->header = psMetadataCopy(targetHDU->header, sourceHDU->header);
+    } else if (!targetHDU->header) {
+        targetHDU->header = psMetadataAlloc();
+    }
+
+    if (!pmConfigConformHeader(targetHDU->header, targetHDU->format)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to conform header after mosaicking.\n");
+        return false;
+    }
+
+    return true;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAMosaic.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAMosaic.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAMosaic.h	(revision 15358)
@@ -0,0 +1,41 @@
+/* @file pmFPAMosaic.h
+ * @brief Functions to mosaic FPA components into a single entity
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.8 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-06-20 02:22:26 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CHIP_MOSAIC_H
+#define PM_CHIP_MOSAIC_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Mosaic all cells within a chip
+///
+/// Mosaics all cells within the source into a single cell within the target (which must have only a single
+/// cell).  Cells are placed on the chip according to the CELL.X0 and CELL.Y0 offsets.  This is useful for
+/// getting an image of the chip on the sky.  The mosaicking is done so as to avoid performing a deep copy of
+/// the pixels, if possible.
+bool pmChipMosaic(pmChip *target,       ///< Target chip --- may contain only a single cell
+                  const pmChip *source, ///< Source chip whose cells will be mosaicked
+                  bool deepCopy,        ///< Require a deep copy (disregard 'nice' chip)
+                  psMaskType blank      ///< Mask value to give blank pixels
+    );
+
+/// Mosaic all cells within an FPA
+///
+/// Mosaics all cells within the source into a single chip with single cell within the target (which must have
+/// only a single chip with single cell).  Cells are placed on the FPA according to the CHIP.X0, CHIP.Y0,
+/// CELL.X0 and CELL.Y0 offsets.  This is useful for getting an image of the FPA on the sky.  The mosaicking
+/// is done so as to avoid performing a deep copy of the pixels, if possible.
+bool pmFPAMosaic(pmFPA *target, ///< Target FPA --- may contain only a single chip with a single cell
+                 const pmFPA *source,   ///< FPA whose chips and cells will be mosaicked
+                 bool deepCopy,         ///< Require a deep copy (disregard 'nice' chip)
+                 psMaskType blank       ///< Mask value to give blank pixels
+                );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPARead.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPARead.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPARead.c	(revision 15358)
@@ -0,0 +1,766 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <assert.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAFlags.h"
+#include "pmHDUUtils.h"
+#include "pmConcepts.h"
+#include "pmFPAHeader.h"
+
+#include "pmFPARead.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Definitions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Specify what to read
+typedef enum {
+    FPA_READ_TYPE_IMAGE,                // Read image
+    FPA_READ_TYPE_MASK,                 // Read mask
+    FPA_READ_TYPE_WEIGHT,               // Read weight map
+    FPA_READ_TYPE_HEADER                // Read header
+} fpaReadType;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Carve a readout from the image pixels
+static bool readoutCarve(pmReadout *readout, // Readout to be carved up
+                         psImage *image, // Image that will be carved
+                         const psRegion *trimsec, // Trim section
+                         const psList *biassecs, // Bias sections
+                         fpaReadType type
+                        )
+{
+    assert(readout);
+    assert(image);
+    assert(trimsec);
+    assert(biassecs);
+
+    // The image corresponding to the trim region
+    if (psRegionIsNaN(*trimsec)) {
+        psString regionString = psRegionToString(*trimsec);
+        psError(PS_ERR_UNKNOWN, true, "Invalid trim section: %s\n", regionString);
+        psFree(regionString);
+        psFree(readout);
+        return false;
+    }
+    psRegion region = psRegionSet(PS_MAX(trimsec->x0 - readout->col0, 0), // x0
+                                  PS_MIN(trimsec->x1 - readout->col0, image->numCols), // x1
+                                  PS_MAX(trimsec->y0 - readout->row0, 0), // y0
+                                  PS_MIN(trimsec->y1 - readout->row0, image->numRows) // y1
+                                 );
+
+    // place the image subset in the appropriate target location, freeing if needed
+    switch (type) {
+      case FPA_READ_TYPE_IMAGE:
+        if (readout->image) {
+            psFree (readout->image);
+        }
+        readout->image = psImageSubset(image, region);
+        break;
+      case FPA_READ_TYPE_MASK:
+        if (readout->mask) {
+            psFree (readout->mask);
+        }
+        readout->mask = psImageSubset(image, region);
+        break;
+      case FPA_READ_TYPE_WEIGHT:
+        if (readout->weight) {
+            psFree (readout->weight);
+        }
+        readout->weight = psImageSubset(image, region);
+        break;
+      default:
+        psAbort("Unknown read type: %x\n", type);
+    }
+
+    // Get the list of overscans
+    // XXX should this step only be performed for IMAGE, not MASK and WEIGHT types?
+    // XXX that would allow us to overlay a MASK and WEIGHT which have been trimmed...
+    if (readout->bias->n != 0) {
+        // Make way!
+        psFree(readout->bias);
+        readout->bias = psListAlloc(NULL);
+    }
+    psListIterator *iter = psListIteratorAlloc((psList*)biassecs, PS_LIST_HEAD, false); // Iterator
+    psRegion *biassec = NULL;       // A BIASSEC region from the list
+    while ((biassec = psListGetAndIncrement(iter))) {
+        if (psRegionIsNaN(*biassec)) {
+            psString regionString = psRegionToString(*biassec);
+            psError(PS_ERR_IO, true, "Invalid bias section: %s\n", regionString);
+            psFree(regionString);
+            psFree(readout);
+            return false;
+        }
+        psRegion region = psRegionSet(PS_MAX(biassec->x0 - readout->col0, 0), // x0
+                                      PS_MIN(biassec->x1 - readout->col0, image->numCols), // x1
+                                      PS_MAX(biassec->y0 - readout->row0, 0), // y0
+                                      PS_MIN(biassec->y1 - readout->row0, image->numRows) // y1
+                                     );
+        psImage *overscan = psImageSubset(image, region);
+        psListAdd(readout->bias, PS_LIST_TAIL, overscan);
+        psFree(overscan);
+    }
+    psFree(iter);
+
+    return true;
+}
+
+// Read a component of a readout.  We read in only the rows from min to max for plane z, for
+// the full region requested.  if we request a range outside the region, we will pad to fill
+// out the edges of the region with 'bad' pixels.  The output image always has max-min rows.
+// The region represents the maximum bounds of the full image
+
+static psImage *readoutReadComponent(psImage *image, // Image into which to read
+                                     psFits *fits, // FITS file from which to read
+                                     const psRegion *fullImage, // full image region, read a subset
+                                     int readdir, // Read direction (1=rows, 2=cols)
+                                     int min,  // Minimum row/col number to read
+                                     int max,   // Maximum row/col number to read
+                                     int z,     // Image plane to read
+                                     float bad // Bad value
+    )
+{
+    assert(fits);
+    assert(fullImage);
+    assert((readdir == 1) || (readdir == 2));
+
+    int nRead = 0;
+    int nScans = max - min;
+    assert (nScans > 0);
+
+    psRegion toRead = *fullImage;  // full image region
+
+    int dX = 0;
+    int dY = 0;
+    int nX = 0;
+    int nY = 0;
+
+    if (readdir == 1) {
+        toRead.y0 = PS_MAX (toRead.y0, min);
+        toRead.y1 = PS_MIN (toRead.y1, max);
+        nRead = toRead.y1 - toRead.y0;
+        if (min < fullImage->y0) {
+            dY = toRead.y0;
+        }
+        nX = toRead.x1 - toRead.x0;
+        nY = nScans;
+    } else {
+        toRead.x0 = PS_MAX (toRead.x0, min);
+        toRead.x1 = PS_MIN (toRead.x1, max);
+        nRead = toRead.x1 - toRead.x0;
+        if (min < fullImage->x0) {
+            dX = toRead.x0;
+        }
+        nX = nScans;
+        nY = toRead.y1 - toRead.y0;
+    }
+
+    psTrace("psModules.camera", 5, "Reading section [%.0f:%.0f,%.0f:%.0f]\n",
+            toRead.x0, toRead.x1, toRead.y0, toRead.y1);
+    image = psFitsReadImageBuffer(image, fits, toRead, z); // Desired pixels
+    psTrace("psModules.camera", 7, "Image is %dx%d\n", image->numCols, image->numRows);
+
+    // XXX: We only support F32 for now
+    if (image->type.type != PS_TYPE_F32) {
+        psImage *temp = psImageCopy(NULL, image, PS_TYPE_F32);
+        psFree(image);
+        image = temp;
+    }
+
+    // XXX this modification is not carried back up stream: it affects readout->row0,col0
+    if (nRead < nScans) {
+        // The region of interest is smaller than the number of pixels we want.
+        psTrace("psModules.camera", 5, "Resizing image to %d,%d\n", nX, nY);
+        psImage *temp = psImageAlloc(nX, nY, image->type.type);
+        psImageInit(temp, bad);
+        psImageOverlaySection(temp, image, dX, dY, "=");
+        psFree(image);
+        image = temp;
+    }
+
+    return image;
+}
+
+
+// Read into an cell; this is the engine for pmCellRead, pmCellReadMask, pmCellReadWeight
+// Does most of the work for the reading --- reads the HDU, and portions the HDU into readouts.
+static bool cellRead(pmCell *cell,      // Cell into which to read
+                     psFits *fits,      // FITS file from which to read
+                     psDB *db,          // Database handle, for concepts ingest
+                     fpaReadType type   // Type to read
+                    )
+{
+    assert(cell);
+    assert(fits);
+
+    pmHDU *hdu = pmHDUFromCell(cell);   // The HDU
+    if (!hdu) {
+        return true;                    // We read everything we could
+    }
+
+    // check if we have read the desired data, read it if needed
+    bool (*hduReadFunc)(pmHDU*, psFits*) = NULL; // Function to use to read the HDU
+    void *dataPointer = NULL;           // pointer to location of desired data
+    switch (type) {
+      case FPA_READ_TYPE_IMAGE:
+        hduReadFunc = pmHDURead;
+        dataPointer = hdu->images;
+        break;
+      case FPA_READ_TYPE_HEADER:
+        hduReadFunc = pmHDUReadHeader;
+        dataPointer = hdu->header;
+        break;
+      case FPA_READ_TYPE_MASK:
+        hduReadFunc = pmHDUReadMask;
+        dataPointer = hdu->masks;
+        break;
+      case FPA_READ_TYPE_WEIGHT:
+        hduReadFunc = pmHDUReadWeight;
+        dataPointer = hdu->weights;
+        break;
+      default:
+        psAbort("Unknown read type: %x\n", type);
+    }
+
+    // do we have the data we want (image, header, or etc).
+    if (!dataPointer) {
+        // attempt to read in the desired data
+        if (!hduReadFunc(hdu, fits)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to read HDU for cell.\n");
+            return false;
+        }
+    }
+
+    // load in the concept information for this cell
+    if (!pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_HEADER, true, NULL)) {
+        //psError(PS_ERR_UNKNOWN, false, "Failed to read concepts for cell");
+        //return false;
+        psWarning("Difficulty reading concepts for cell; attempting to proceed.");
+    }
+
+    // skip the image arrays completely for the header-only files
+    if (type == FPA_READ_TYPE_HEADER) {
+        pmCellSetDataStatus(cell, true);
+        return true;
+    }
+
+    // set up pointers for the different possible image arrays
+    psArray *imageArray = NULL; // Array of images in the HDU
+    psElemType imageType = PS_TYPE_F32; // Expected type for image
+    switch (type) {
+      case FPA_READ_TYPE_IMAGE:
+        imageArray = hdu->images;
+        imageType = PS_TYPE_F32;
+        break;
+      case FPA_READ_TYPE_MASK:
+        imageArray = hdu->masks;
+        imageType = PS_TYPE_MASK;
+        break;
+      case FPA_READ_TYPE_WEIGHT:
+        imageArray = hdu->weights;
+        imageType = PS_TYPE_F32;
+        break;
+      default:
+        psAbort("Unknown read type: %x\n", type);
+    }
+
+    // Having read the cell, we now have to cut it up
+    psRegion *trimsec = psMetadataLookupPtr(NULL, cell->concepts, "CELL.TRIMSEC");
+    psList *biassecs = psMetadataLookupPtr(NULL, cell->concepts, "CELL.BIASSEC");
+    if (psRegionIsNaN(*trimsec)) {
+        psError(PS_ERR_IO, false, "CELL.TRIMSEC is not set --- can't read cell.\n");
+        return false;
+    }
+
+    // Iterate over each of the image planes, converting type if necessary, and extracting the bits that
+    // matter (CELL.TRIMSEC, CELL.BIASSEC) into readouts with readoutCarve.
+    for (int i = 0; i < imageArray->n; i++) {
+        psImage *source = imageArray->data[i]; // Source image, from the i-th plane
+
+        // Type conversion here to support the modules, which don't have multiple type support yet
+        if (source->type.type != imageType) {
+            psImage *temp = psImageCopy(NULL, source, imageType); // Temporary image
+            psFree(imageArray->data[i]);
+            imageArray->data[i] = temp;
+            source = temp;
+        }
+
+        pmReadout *readout;             // Readout into which to read
+        if (cell->readouts->n > i && cell->readouts->data[i]) {
+            readout = psMemIncrRefCounter(cell->readouts->data[i]);
+        } else {
+            readout = pmReadoutAlloc(cell);
+        }
+
+        if (!readoutCarve(readout, source, trimsec, biassecs, type)) {
+            psError(PS_ERR_UNEXPECTED_NULL, false,
+                    "Unable to carve readout into image and bias sections for %d the plane.", i);
+            return NULL;
+        }
+        psFree(readout);                // Drop reference
+    }
+
+    pmCellSetDataStatus(cell, true);
+    return true;
+}
+
+
+// Read into an chip; this is the engine for pmChipRead, pmChipReadMask, pmChipReadWeight
+// Iterates over component cells, reading each
+static bool chipRead(pmChip *chip,      // Chip into which to read
+                     psFits *fits,      // FITS file from which to read
+                     psDB *db,          // Database handle, for concepts ingest
+                     fpaReadType type   // Type to read
+                    )
+{
+    assert(chip);
+    assert(fits);
+
+    bool success = false;               // Were we able to read at least one HDU?
+    psArray *cells = chip->cells;       // Array of cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // The cell of interest
+        success |= cellRead(cell, fits, db, type);
+    }
+    if (success) {
+        if (!pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_HEADER, true, true, NULL)) {
+            psError(PS_ERR_IO, false, "Failed to read concepts for chip.\n");
+            return false;
+        }
+        // XXX probably could just use chip->data_exists
+        pmChipSetDataStatus(chip, true);
+    }
+
+    return success;
+}
+
+
+// Read into an FPA; this is the engine for pmFPARead, pmFPAReadMask, pmFPAReadWeight
+// Iterates over component chips, reading each
+static bool fpaRead(pmFPA *fpa,         // FPA into which to read
+                    psFits *fits,       // FITS file from which to read
+                    psDB *db,           // Database handle, for concepts ingest
+                    fpaReadType type    // Type to read
+                   )
+{
+    assert(fpa);
+    assert(fits);
+
+    bool success = false;               // Were we able to read at least one HDU?
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // The cell of interest
+        success |= chipRead(chip, fits, db, type);
+    }
+    if (success) {
+        if (!pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_HEADER, true, NULL)) {
+            psError(PS_ERR_IO, false, "Failed to read concepts for FPA.\n");
+            return false;
+        }
+    } else {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read any chips in FPA");
+    }
+
+    return success;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Reading images
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// XXX need to pass back the error conditions as well as the 'keep going' state
+bool pmReadoutReadNext(bool *status, pmReadout *readout, psFits *fits, int z, int numScans)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_INT_NONNEGATIVE(z, false);
+    PS_ASSERT_INT_NONNEGATIVE(numScans, false);
+
+    assert (numScans || !readout->image); // cannot have readout->image and !numScans
+
+    *status = false;
+
+    // Get the HDU and read the header
+    pmCell *cell = readout->parent;     // The parent cell
+
+    pmHDU *hdu = pmHDUFromCell(cell);   // The HDU
+    if (!hdu || hdu->blankPHU) {
+        // XXX is this an error condition?
+        *status = true;
+        return false;
+    }
+
+    if (!pmCellReadHeader(cell, fits)) {
+        psError(PS_ERR_IO, false, "Unable to read header for cell!\n");
+        return false;
+    }
+
+    // Make sure we have the information we need
+    if (!pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_HEADER | PM_CONCEPT_SOURCE_CELLS |
+                            PM_CONCEPT_SOURCE_DEFAULTS, true, NULL)) {
+        psError(PS_ERR_IO, false, "Failed to read concepts for cell.\n");
+        return false;
+    }
+
+    // Get the trim and bias sections
+    bool mdok = true;                   // Status of MD lookup
+    psRegion *trimsec = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.TRIMSEC"); // Trim sections
+    if (!mdok || !trimsec || psRegionIsNaN(*trimsec)) {
+        psError(PS_ERR_IO, true, "CELL.TRIMSEC is not set.\n");
+        return false;
+    }
+    psList *biassecs = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.BIASSEC"); // Bias sections
+    if (!mdok || !biassecs) {
+        psError(PS_ERR_IO, true, "CELL.BIASSEC is not set.\n");
+        return false;
+    }
+    int readdir = psMetadataLookupS32(&mdok, cell->concepts, "CELL.READDIR"); // Read direction
+    if (!mdok || readdir == 0 || (readdir != 1 && readdir != 2)) {
+        psError(PS_ERR_IO, true, "CELL.READDIR is not set to -1 or +1.\n");
+        return false;
+    }
+    float bad = psMetadataLookupF32(&mdok, cell->concepts, "CELL.BAD"); // Bad level
+    if (!mdok) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.BAD is not set --- assuming zero.\n");
+        bad = 0.0;
+    }
+
+    // Check the third dimension
+    int naxis = psMetadataLookupS32(&mdok, hdu->header, "NAXIS"); // The number of axes
+    if (!mdok) {
+        psError(PS_ERR_IO, true, "Unable to find NAXIS in header for extension %s\n", hdu->extname);
+        return false;
+    }
+    if (naxis == 0) {
+        // No pixels to read, as for a PHU.
+        *status = true;
+        return false;
+    }
+    if (naxis < 2 || naxis > 3) {
+        psError(PS_ERR_IO, true, "NAXIS in header of extension %s (= %d) is not valid.\n",
+                hdu->extname, naxis);
+        return false;
+    }
+    int naxis3 = 1;                     // The number of image planes
+    if (naxis == 3) {
+        naxis3 = psMetadataLookupS32(&mdok, hdu->header, "NAXIS3");
+        if (!mdok) {
+            psError(PS_ERR_IO, true, "Unable to find NAXIS3 in header for extension %s\n", hdu->extname);
+            return false;
+        }
+    }
+    if (z >= naxis3) {
+        // Nothing to see here.  Move along.
+        *status = true;
+        return false;
+    }
+
+    // Get the size of the image plane
+    int naxis1 = psMetadataLookupS32(&mdok, hdu->header, "NAXIS1"); // The number of columns
+    if (!mdok) {
+        psError(PS_ERR_IO, true, "Unable to find NAXIS1 in header for extension %s\n", hdu->extname);
+        return false;
+    }
+    int naxis2 = psMetadataLookupS32(&mdok, hdu->header, "NAXIS2"); // The number of columns
+    if (!mdok) {
+        psError(PS_ERR_IO, true, "Unable to find NAXIS2 in header for extension %s\n", hdu->extname);
+        return false;
+    }
+
+    // rationalize trimsec against naxis1, naxis2
+    // valid range for trimsec is 1-Nx,1-Ny
+    // if (trimsec->x0 == 0) trimsec->x0 = 1;
+    if (trimsec->x1 <  1)
+        trimsec->x1 = naxis1 + trimsec->x1;
+    // if (trimsec->y0 == 0) trimsec->y0 = 1;
+    if (trimsec->y1 <  1)
+        trimsec->y1 = naxis2 + trimsec->y1;
+
+    int maxSize;                        // Number of cols,rows in image
+    if (readdir == 1) {
+        maxSize = PS_MIN(naxis2, trimsec->y1 - trimsec->y0);
+    } else {
+        maxSize = PS_MIN(naxis1, trimsec->x1 - trimsec->x0);
+    }
+
+    int offset;                         // start of the segment
+    int upper;                          // end of the segment
+    int lastScan;                       // last possible scan
+
+    // Calculate the segment offset and upper limit, adjust readout->row0,col0
+    if (readdir == 1) {
+        // Reading rows
+        offset = (readout->image) ? readout->row0 + numScans : 0; // extend to next section or start at beginning?
+        offset = (numScans == 0) ? trimsec->x0 : offset; // full array ? read full trimsec : read section
+        readout->row0 = offset;
+        readout->col0 = trimsec->x0;
+        lastScan = trimsec->y1;
+    } else {
+        // Reading cols
+        offset = (readout->image) ? readout->col0 + numScans : 0;
+        offset = (numScans == 0) ? trimsec->y0 : offset; // full array ? read full trimsec : read section
+        readout->col0 = offset;
+        readout->row0 = trimsec->y0;
+        lastScan = trimsec->x1;
+    }
+    upper = offset + numScans;
+
+    // Blow away existing data.
+    // Do this before returning, so that we're not returning data from a previous read
+    psFree(readout->image);
+    readout->image = NULL;
+
+    while (readout->bias->n > 0) {
+        psListRemove(readout->bias, PS_LIST_HEAD);
+    }
+
+    if (offset >= lastScan) {
+        // We've read everything there is
+        psTrace("psModules.camera", 7, "Read everything.\n");
+        *status = true;
+        return false;
+    }
+
+    psTrace("psModules.camera", 7, "offset=%d, upper = %d, image is %dx%d, trimsec [%.0f:%.0f,%.0f:%.0f]\n",
+            offset, upper, naxis1, naxis2, trimsec->x0, trimsec->x1, trimsec->y0, trimsec->y1);
+
+    // Get the new the trim section
+    readout->image = readoutReadComponent(readout->image, fits, trimsec, readdir, offset, upper, z, bad); // The image
+
+    // Get the new bias sections
+    psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false); // Iterator for BIASSEC
+    psRegion *biassec = NULL;           // Bias section from iteration
+    while ((biassec = psListGetAndIncrement(biassecsIter))) {
+        psImage *bias = readoutReadComponent(NULL, fits, biassec, readdir, offset, upper, z, bad); // The bias
+        psListAdd(readout->bias, PS_LIST_TAIL, bias);
+        psFree(bias);                   // Drop reference
+    }
+    psFree(biassecsIter);
+
+    *status = true;
+    return true;
+}
+
+
+bool pmCellRead(pmCell *cell, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return cellRead(cell, fits, db, FPA_READ_TYPE_IMAGE);
+}
+
+bool pmChipRead(pmChip *chip, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return chipRead(chip, fits, db, FPA_READ_TYPE_IMAGE);
+}
+
+bool pmFPARead(pmFPA *fpa, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return fpaRead(fpa, fits, db, FPA_READ_TYPE_IMAGE);
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Reading the mask
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmCellReadMask(pmCell *cell, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return cellRead(cell, fits, db, FPA_READ_TYPE_MASK);
+}
+
+bool pmChipReadMask(pmChip *chip, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return chipRead(chip, fits, db, FPA_READ_TYPE_MASK);
+}
+
+bool pmFPAReadMask(pmFPA *fpa, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return fpaRead(fpa, fits, db, FPA_READ_TYPE_MASK);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Reading the weight map
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmCellReadWeight(pmCell *cell, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return cellRead(cell, fits, db, FPA_READ_TYPE_WEIGHT);
+}
+
+bool pmChipReadWeight(pmChip *chip, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return chipRead(chip, fits, db, FPA_READ_TYPE_WEIGHT);
+}
+
+bool pmFPAReadWeight(pmFPA *fpa, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return fpaRead(fpa, fits, db, FPA_READ_TYPE_WEIGHT);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Reading the image header
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmCellReadHeaderSet(pmCell *cell, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return cellRead(cell, fits, db, FPA_READ_TYPE_HEADER);
+}
+
+bool pmChipReadHeaderSet(pmChip *chip, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return chipRead(chip, fits, db, FPA_READ_TYPE_HEADER);
+}
+
+bool pmFPAReadHeaderSet(pmFPA *fpa, psFits *fits, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return fpaRead(fpa, fits, db, FPA_READ_TYPE_HEADER);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Reading FITS tables
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+int pmCellReadTable(pmCell *cell, psFits *fits, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, 0);
+    PS_ASSERT_PTR_NON_NULL(fits, 0);
+    PS_ASSERT_STRING_NON_EMPTY(name, 0);
+
+    const char *chipName = psMetadataLookupStr(NULL, cell->parent->concepts, "CHIP.NAME"); // Name of chip
+    const char *cellName = psMetadataLookupStr(NULL, cell->concepts, "CELL.NAME"); // Name of cell
+    psString extname = NULL;            // Extension name
+    psStringAppend(&extname, "%s_%s_%s", name, chipName, cellName);
+
+    // XXX Could do a table lookup from the camera format, in case the input file isn't laid out with
+    // NAME_CHIP_CELL extension names --- use these as keys, and the value as the proper extension name.
+    // Allow interpolation of concepts, e.g., "{CHIP.NAME}" --> "ccd13".
+
+    if (!psFitsMoveExtName(fits, extname)) {
+        psError(PS_ERR_IO, false, "Unable to move to extension %s\n", extname);
+        psFree(extname);
+        return 0;
+    }
+
+    psMetadata *header = psFitsReadHeader(NULL, fits); // The FITS header
+    if (!header) {
+        psError(PS_ERR_IO, false, "Unable to read header for extension %s\n", extname);
+        psFree(extname);
+        psFree(header);
+        return 0;
+    }
+
+    psString headerName = NULL;         // Name for header
+    psStringAppend(&headerName, "%s.HEADER", name);
+    if (!psMetadataAdd(cell->analysis, PS_LIST_TAIL, headerName, PS_DATA_METADATA,
+                       "FITS table header", header)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to add header from extension %s to analysis metadata "
+                "for chip %s, cell %s\n", extname, chipName, cellName);
+        psFree(headerName);
+        psFree(header);
+        psFree(extname);
+        return 0;
+    }
+    psFree(headerName);
+    psFree(header);
+
+    psArray *table = psFitsReadTable(fits); // The table
+    if (!psMetadataAdd(cell->analysis, PS_LIST_TAIL, name, PS_DATA_ARRAY, "FITS table", table)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to add table from extension %s to analysis metadata "
+                "for chip %s, cell %s\n", extname, chipName, cellName);
+        psFree(table);
+        psFree(extname);
+        return 0;
+    }
+
+    psFree(extname);
+    psFree(table);                      // Dropping reference
+
+    return 1;
+}
+
+
+int pmChipReadTable(pmChip *chip, psFits *fits, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, 0);
+    PS_ASSERT_PTR_NON_NULL(fits, 0);
+    PS_ASSERT_STRING_NON_EMPTY(name, 0);
+
+    int numRead = 0;                    // Number of reads
+    psArray *cells = chip->cells;       // Array of cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // Cell of interest
+        numRead += pmCellReadTable(cell, fits, name);
+    }
+
+    return numRead;
+}
+
+
+int pmFPAReadTable(pmFPA *fpa, psFits *fits, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, 0);
+    PS_ASSERT_PTR_NON_NULL(fits, 0);
+    PS_ASSERT_STRING_NON_EMPTY(name, 0);
+
+    int numRead = 0;                    // Number of reads
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        numRead += pmChipReadTable(chip, fits, name);
+    }
+
+    return numRead;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPARead.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPARead.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPARead.h	(revision 15358)
@@ -0,0 +1,157 @@
+/* @file pmFPARead.h
+ * @brief Functions to read FPA components from a FITS file
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-06-12 22:22:33 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_READ_H
+#define PM_FPA_READ_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Read a readout incrementally
+///
+/// Multiple calls to this function moves through a readout within a cell incrementally.  It is required to
+/// pass the readout previously acquired (or a newly-allocated one) in order to preserve state information
+/// (where the read is at).  Only a maximum of numRows rows are read at a time.  Returns true if pixels are
+/// read, and false otherwise.  To facilitate looping, no error is generated for reading a plane that doesn't
+/// exist.  Note that this function doesn't put pixels in the HDU.  It is therefore NOT COMPATIBLE with
+/// pmCellWrite, pmChipWrite, and pmFPAWrite (or any function that uses or creates an HDU for that matter).
+/// Use pmReadoutWriteNext to write the data that's read by this function.  This function is intended for
+/// reading in many readouts into memory at once (e.g., for stacking) where the input is not written out.
+bool pmReadoutReadNext(bool *status,	// non-error exit condition?
+		       pmReadout *readout, // Readout into which to read
+                       psFits *fits,    // FITS file from which to read
+                       int z,           // Readout number/plane; zero-offset indexing
+                       int numRows      // The number of rows to read
+                      );
+
+/// Read an entire cell
+///
+/// Reads the appropriate HDU, ingests concepts from the header, and portions pixels into readouts.  Pixels
+/// are converted to F32.
+bool pmCellRead(pmCell *cell,           // Cell to read into
+                psFits *fits,           // FITS file from which to read
+                psDB *db                // Database handle, for "concepts" ingest
+               );
+
+/// Read a chip
+///
+/// Iterates over component cells, reading each with pmCellRead.
+bool pmChipRead(pmChip *chip,           // Chip to read into
+                psFits *fits,           // FITS file from which to read
+                psDB *db                // Database handle, for "concepts" ingest
+               );
+
+/// Read an FPA
+///
+/// Iterates over component chips, reading each with pmChipRead.
+bool pmFPARead(pmFPA *fpa,              // FPA to read into
+               psFits *fits,            // FITS file from which to read
+               psDB *db                 // Database handle, for "concepts" ingest
+              );
+
+/// Read an entire cell into the mask
+///
+/// Same as pmCellRead, but reads into the mask element of the readouts.
+bool pmCellReadMask(pmCell *cell,           // Cell to read into
+                    psFits *fits,           // FITS file from which to read
+                    psDB *db                // Database handle, for "concepts" ingest
+                   );
+
+/// Read an entire chip into the mask
+///
+/// Same as pmChipRead, but reads into the mask element of the readouts.
+bool pmChipReadMask(pmChip *chip,           // Chip to read into
+                    psFits *fits,           // FITS file from which to read
+                    psDB *db                // Database handle, for "concepts" ingest
+                   );
+
+/// Read an entire FPA into the mask
+///
+/// Same as pmFPARead, but reads into the mask element of the readouts.
+bool pmFPAReadMask(pmFPA *fpa,              // FPA to read into
+                   psFits *fits,            // FITS file from which to read
+                   psDB *db                 // Database handle, for "concepts" ingest
+                  );
+
+/// Read an entire cell into the weight
+///
+/// Same as pmCellRead, but reads into the weight element of the readouts.
+bool pmCellReadWeight(pmCell *cell,           // Cell to read into
+                      psFits *fits,           // FITS file from which to read
+                      psDB *db                // Database handle, for "concepts" ingest
+                     );
+
+/// Read an entire chip into the weight
+///
+/// Same as pmChipRead, but reads into the weight element of the readouts.
+bool pmChipReadWeight(pmChip *chip,           // Chip to read into
+                      psFits *fits,           // FITS file from which to read
+                      psDB *db                // Database handle, for "concepts" ingest
+                     );
+
+/// Read an entire FPA into the weight
+///
+/// Same as pmFPARead, but reads into the weight element of the readouts.
+bool pmFPAReadWeight(pmFPA *fpa,              // FPA to read into
+                     psFits *fits,            // FITS file from which to read
+                     psDB *db                 // Database handle, for "concepts" ingest
+                    );
+
+/// Read cell headers
+///
+/// Same as pmCellRead, but reads only the headers of the readouts.
+bool pmCellReadHeaderSet(pmCell *cell,           // Cell to read into
+			 psFits *fits,           // FITS file from which to read
+			 psDB *db                // Database handle, for "concepts" ingest
+    );
+
+/// Read chip headers
+///
+/// Same as pmChipRead, but reads only the headers of the readouts.
+bool pmChipReadHeaderSet(pmChip *chip,           // Chip to read into
+                      psFits *fits,           // FITS file from which to read
+                      psDB *db                // Database handle, for "concepts" ingest
+                     );
+
+/// Read FPA headers
+///
+/// Same as pmFPARead, but reads only the headers of the readouts.
+bool pmFPAReadHeaderSet(pmFPA *fpa,              // FPA to read into
+			psFits *fits,            // FITS file from which to read
+			psDB *db                 // Database handle, for "concepts" ingest
+    );
+
+/// Read a FITS table into the cell
+///
+/// Given a name, which is combined with the chip and cell to identify the extension name ("NAME_CHIP_CELL"),
+/// read the FITS table into the cell analysis metadata (with key being the provided name).  The header is
+/// also read and included in the cell analysis metadata under "name.HEADER".
+int pmCellReadTable(pmCell *cell,       ///< Cell for which to read table
+                    psFits *fits,       ///< FITS file from which the table
+                    const char *name    ///< Specifies the extension name, and target in the analysis metadata
+                   );
+
+/// Read a FITS table into the component cells
+///
+/// Iterates over component cells, calling pmCellReadTable.
+int pmChipReadTable(pmChip *chip,       ///< Cell for which to read table
+                    psFits *fits,       ///< FITS file from which the table
+                    const char *name    ///< Specifies the extension name, and target in the analysis metadata
+                   );
+
+/// Read a FITS table into the component cells
+///
+/// Iterates over component chips, calling pmChipReadTable.
+int pmFPAReadTable(pmFPA *fpa,          ///< Cell for which to read table
+                   psFits *fits,        ///< FITS file from which the table
+                   const char *name     ///< Specifies the extension name, and target in the analysis metadata
+                  );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAUtils.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAUtils.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAUtils.c	(revision 15358)
@@ -0,0 +1,55 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAUtils.h"
+
+int pmFPAFindChip(const pmFPA *fpa, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, -1);
+    PS_ASSERT_PTR_NON_NULL(name, -1);
+    if (strlen(name) == 0) {
+        return -1;
+    }
+
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i]; // The chip of interest
+        psString testName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME"); // Name of this chip
+        if (strcmp(name, testName) == 0) {
+            return i;
+        }
+    }
+
+    psError(PS_ERR_IO, true, "Unable to find chip %s\n", name);
+    return -1;
+}
+
+
+int pmChipFindCell(const pmChip *chip, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, -1);
+    PS_ASSERT_PTR_NON_NULL(name, -1);
+    if (strlen(name) == 0) {
+        return -1;
+    }
+
+    psArray *cells = chip->cells;    // Array of cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i]; // The cell of interest
+        psString testName = psMetadataLookupStr(NULL, cell->concepts, "CELL.NAME"); // Name of this cell
+        if (strcmp(name, testName) == 0) {
+            return i;
+        }
+    }
+
+    psError(PS_ERR_IO, true, "Unable to find cell %s\n", name);
+    return -1;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAUtils.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAUtils.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAUtils.h	(revision 15358)
@@ -0,0 +1,33 @@
+/* @file pmFPAUtils.h
+ * @brief Utility functions for FPAs
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_UTILS_H
+#define PM_FPA_UTILS_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Find a chip by name; return the index
+///
+/// Looks for a chip within the FPA with CHIP.NAME matching the provided name.  Returns the index of the chip,
+/// or -1 if it was not found.
+int pmFPAFindChip(const pmFPA *fpa,     ///< FPA in which to find the chip
+                  const char *name      ///< Name of the chip
+                 );
+
+/// Find a cell by name; return the index
+///
+/// Looks for a cell within the chip with CELL.NAME matching the provided name.  Returns the index of the
+/// cell, or -1 if it was not found.
+int pmChipFindCell(const pmChip *chip,  // Chip in which to find the cell
+                   const char *name     // Name of the cell
+                  );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAWrite.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAWrite.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAWrite.c	(revision 15358)
@@ -0,0 +1,508 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmDetrendDB.h"
+#include "pmFPAfile.h"
+#include "pmHDUUtils.h"
+#include "pmHDUGenerate.h"
+#include "pmConcepts.h"
+
+#include "pmFPAWrite.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Definitions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Specify what to read
+typedef enum {
+    FPA_WRITE_TYPE_IMAGE,               // Write image
+    FPA_WRITE_TYPE_MASK,                // Write mask
+    FPA_WRITE_TYPE_WEIGHT               // Write weight map
+} fpaWriteType;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static (private) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+// Update the FPA.NAME, CHIP.NAME and CELL.NAME in the FITS header, if required
+bool pmFPAUpdateNames(pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    pmHDU *hdu = pmHDUGetHighest(fpa, chip, cell); // Highest HDU, i.e., the PHU
+    if (!hdu) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find HDU.\n");
+        return false;
+    }
+    if (!hdu->header) {
+        hdu->header = psMetadataAlloc();
+    }
+    bool mdok;                          // Status of MD lookup
+    psMetadata *fileData = psMetadataLookupMetadata(&mdok, hdu->format, "FILE"); // File information
+    if (!mdok || !fileData) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find FILE information in camera format.\n");
+        return false;
+    }
+    if (fpa) {
+        const char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+        if (mdok && fpaNameHdr && strlen(fpaNameHdr) > 0) {
+            const char *fpaName = psMetadataLookupStr(NULL, fpa->concepts, "FPA.NAME");
+            psMetadataAddStr(hdu->header, PS_LIST_TAIL, fpaNameHdr, PS_META_REPLACE, "FPA name", fpaName);
+        }
+    }
+
+    if (fpa && !fpa->hdu && (chip || cell)) {
+        const char *rule = psMetadataLookupStr(NULL, fileData, "CONTENT.RULE"); // How to define the CONTENT
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to find CONTENT.RULE in FILE in camera format.");
+            return false;
+        }
+
+        pmFPAview *view = pmFPAviewGenerate(fpa, chip, cell, NULL); // View for fpa, chip, cell
+        psString content = pmFPANameFromRule(rule, fpa, view); // Content of this file, specified by the rule
+        psFree(view);
+
+        const char *contentKey = psMetadataLookupStr(NULL, fileData, "CONTENT"); // The CONTENT header keyword
+        if (!contentKey) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to find CONTENT in FILE in the camera format.");
+            psFree(content);
+            return false;
+        }
+
+        psMetadataAddStr(hdu->header, PS_LIST_TAIL, contentKey, PS_META_REPLACE, "Content of file", content);
+        psFree(content);                // Drop reference
+    }
+
+    return true;
+}
+
+// Return the appropriate image array for the given type
+static psArray **appropriateImageArray(pmHDU *hdu, // HDU containing the image arrays
+                                       fpaWriteType type // Type to write
+                                      )
+{
+    switch (type) {
+    case FPA_WRITE_TYPE_IMAGE:
+        return &hdu->images;
+    case FPA_WRITE_TYPE_MASK:
+        return &hdu->masks;
+    case FPA_WRITE_TYPE_WEIGHT:
+        return &hdu->weights;
+    default:
+        psAbort("Unknown write type: %x\n", type);
+    }
+    return NULL;
+}
+
+// Run the appropriate HDU write function
+static bool appropriateWriteFunc(pmHDU *hdu, // HDU to write
+                                 psFits *fits, // FITS file to which to write
+                                 fpaWriteType type // Type to write
+                                )
+{
+    switch (type) {
+    case FPA_WRITE_TYPE_IMAGE:
+        return pmHDUWrite(hdu, fits);
+    case FPA_WRITE_TYPE_MASK:
+        return pmHDUWriteMask(hdu, fits);
+    case FPA_WRITE_TYPE_WEIGHT:
+        return pmHDUWriteWeight(hdu, fits);
+    default:
+        psAbort("Unknown write type: %x\n", type);
+    }
+    return false;
+}
+// Write a cell image/mask/weight
+static bool cellWrite(pmCell *cell,     // Cell to write
+                      psFits *fits,     // FITS file to which to write
+                      psDB *db,         // Database handle for "concepts" update
+                      bool blank,       // Write a blank PHU?
+                      fpaWriteType type // Type to write
+                     )
+{
+    assert(cell);
+    assert(fits);
+
+    psTrace ("pmFPAWrite", 5, "writing to Cell (%d)\n", blank);
+
+    pmHDU *hdu = cell->hdu;             // The HDU
+    if (!hdu || !cell->data_exists) {
+        return true;                    // We wrote every HDU that exists
+    }
+
+    psArray **imageArray = appropriateImageArray(hdu, type); // Array of images in the HDU
+
+    // Generate the HDU if needed --- this is required after a pmFPACopy, or similar, which does not
+    // generate the HDU, but only copies the structure.
+    if (!blank && !hdu->blankPHU && !*imageArray && (!pmHDUGenerateForCell(cell) || !*imageArray)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate HDU for cell --- likely programming error.\n");
+        return false;
+    }
+
+    // We only write out a blank PHU if it's specifically requested.
+    bool writeBlank = blank && hdu->blankPHU && !*imageArray; // Write a blank PHU?
+    bool writeImage = !blank && !hdu->blankPHU && *imageArray; // Write an image?
+
+    if (writeBlank || writeImage) {
+
+        pmFPAUpdateNames(cell->parent->parent, cell->parent, cell);
+        pmConceptSource source = PM_CONCEPT_SOURCE_HEADER | PM_CONCEPT_SOURCE_CELLS |
+                                 PM_CONCEPT_SOURCE_DEFAULTS;
+        if (!pmConceptsWriteCell(cell, source, true, NULL)) {
+            psError(PS_ERR_IO, false, "Unable to write concepts for cell.\n");
+            return false;
+        }
+        if (!appropriateWriteFunc(hdu, fits, type)) {
+            psError(PS_ERR_IO, false, "Unable to write HDU for cell.\n");
+            return false;
+        }
+    }
+    // No lower levels to which to recurse
+
+    return true;
+}
+
+// Write a chip image/mask/weight
+static bool chipWrite(pmChip *chip,     // Chip to write
+                      psFits *fits,     // FITS file to which to write
+                      psDB *db,         // Database handle for "concepts" update
+                      bool blank,       // Write a blank PHU?
+                      bool recurse,     // Recurse to lower levels?
+                      fpaWriteType type // Type to write
+                     )
+{
+    assert(chip);
+    assert(fits);
+
+    pmHDU *hdu = chip->hdu;             // The HDU
+
+    psTrace ("pmFPAWrite", 5, "writing to Chip (%d, %d)\n", blank, recurse);
+
+    // If we have data at this level, try to write it out
+    if (hdu && chip->data_exists) {
+        psArray **imageArray = appropriateImageArray(hdu, type); // Array of images in HDU
+
+        // Generate the HDU if needed --- this is required after a pmFPACopy, or similar, which does not
+        // generate the HDU, but only copies the structure.
+        if (!blank && !hdu->blankPHU && !*imageArray && (!pmHDUGenerateForChip(chip) || !*imageArray)) {
+            psError(PS_ERR_UNKNOWN, false,
+                    "Unable to generate HDU for chip --- likely programming error.\n");
+            return false;
+        }
+
+        // We only write out a blank PHU if it's specifically requested.
+        bool writeBlank = blank && hdu->blankPHU && !*imageArray; // Write a blank HDU?
+        bool writeImage = !blank && !hdu->blankPHU && *imageArray; // Write an image?
+
+        if (writeBlank || writeImage) {
+            pmFPAUpdateNames(chip->parent, chip, NULL);
+            pmConceptSource source = PM_CONCEPT_SOURCE_HEADER | PM_CONCEPT_SOURCE_CELLS |
+                                     PM_CONCEPT_SOURCE_DEFAULTS;
+            if (!pmConceptsWriteChip(chip, source, true, true, NULL)) {
+                psError(PS_ERR_IO, false, "Unable to write concepts for chip.\n");
+                return false;
+            }
+            if (!appropriateWriteFunc(hdu, fits, type)) {
+                psError(PS_ERR_IO, false, "Unable to write HDU for chip.\n");
+                return false;
+            }
+        }
+    }
+
+    // Recurse to lower level if specifically requested.
+    // XXX recursion implies blank == false (must be called on correct level?)
+    if (recurse) {
+        psArray *cells = chip->cells;       // Array of cells
+        for (int i = 0; i < cells->n; i++) {
+            pmCell *cell = cells->data[i];  // The cell of interest
+            if (!cellWrite(cell, fits, db, false, type)) {
+                psError(PS_ERR_IO, false, "Unable to write Chip.\n");
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+
+// Write an FPA image/mask/weight
+static bool fpaWrite(pmFPA *fpa,        // FPA to write
+                     psFits *fits,      // FITS file to which to write
+                     psDB *db,          // Database handle for "concepts" update
+                     bool blank,        // Write a blank PHU?
+                     bool recurse,      // Recurse to lower levels?
+                     fpaWriteType type  // Type to write
+                    )
+{
+    assert(fpa);
+    assert(fits);
+
+    pmHDU *hdu = fpa->hdu;              // The HDU
+
+    psTrace ("pmFPAWrite", 5, "writing to FPA (%d, %d)\n", blank, recurse);
+
+    // If we have data at this level, try to write it out
+    if (hdu) {
+        psArray **imageArray = appropriateImageArray(hdu, type); // Array of images in HDU
+
+        // Generate the HDU if needed --- this is required after a pmFPACopy, or similar, which does not
+        // generate the HDU, but only copies the structure.
+        if (!blank && !hdu->blankPHU && !*imageArray && (!pmHDUGenerateForFPA(fpa) || !*imageArray)) {
+            psError(PS_ERR_UNKNOWN, false,
+                    "Unable to generate HDU for FPA --- likely programming error.\n");
+            return false;
+        }
+
+        // We only write out a blank PHU if it's specifically requested.
+        bool writeBlank = blank && hdu->blankPHU && !*imageArray; // Write a blank PHU?
+        bool writeImage = !blank && !hdu->blankPHU && *imageArray; // Write an image?
+
+        if (writeBlank || writeImage) {
+            pmFPAUpdateNames(fpa, NULL, NULL);
+            pmConceptSource source = PM_CONCEPT_SOURCE_HEADER | PM_CONCEPT_SOURCE_CELLS |
+                                     PM_CONCEPT_SOURCE_DEFAULTS;
+            if (!pmConceptsWriteFPA(fpa, source, true, NULL)) {
+                psError(PS_ERR_IO, false, "Unable to write concepts for FPA.\n");
+                return false;
+            }
+            if (!appropriateWriteFunc(hdu, fits, type))  {
+                psError(PS_ERR_IO, false, "Unable to write HDU for FPA.\n");
+                return false;
+            }
+        }
+    }
+
+    // Recurse to lower levels if requested
+    if (recurse) {
+        psArray *chips = fpa->chips;        // Array of chips
+        for (int i = 0; i < chips->n; i++) {
+            pmChip *chip = chips->data[i];  // The chip of interest
+            if (!chipWrite(chip, fits, db, false, true, type)) {
+                psError(PS_ERR_IO, false, "Unable to write FPA.\n");
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmReadoutWriteNext(pmReadout *readout, psFits *fits, int z)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    pmHDU *hdu = pmHDUFromReadout(readout); // The HDU to which to write
+    if (!hdu) {
+        psError(PS_ERR_IO, false, "Unable to find HDU for readout.\n");
+        return false;
+    }
+    psMetadata *header = hdu->header;   // The FITS header
+    if (!header) {
+        psError(PS_ERR_IO, true, "No FITS header available in the HDU.\n");
+        return false;
+    }
+
+    // We have to rely to a great extent on the FITS header, because otherwise we simply don't know how many
+    // image planes there are (NAXIS3) or the size of the original image (NAXIS1, NAXIS2).
+    bool mdok = true;                   // Status of MD lookup
+    int naxis1 = psMetadataLookupS32(&mdok, header, "NAXIS1"); // Number of columns
+    if (!mdok || naxis1 <= 0) {
+        psError(PS_ERR_IO, true, "Can't find NAXIS1 in header.\n");
+        return false;
+    }
+    int naxis2 = psMetadataLookupS32(&mdok, header, "NAXIS2"); // Number of rows
+    if (!mdok || naxis2 <= 0) {
+        psError(PS_ERR_IO, true, "Can't find NAXIS2 in header.\n");
+        return false;
+    }
+    int naxis3 = psMetadataLookupS32(&mdok, header, "NAXIS3"); // Number of image planes
+    if (!mdok || naxis3 <= 0) {
+        naxis3 = 1;
+    }
+    if (z >= naxis3) {
+        psError(PS_ERR_IO, true, "Specified a plane number (%d) greater than NAXIS3 allows.\n", z);
+        return false;
+    }
+
+    if (!hdu->images) {
+        psError(PS_ERR_IO, true, "No images allocated in HDU.\n");
+        return false;
+    }
+    psImage *image = readout->image; // The image from the HDU to write
+    if (readout->row0 == 0 && readout->col0 == 0 && z == 0) {
+        // Then we can assume that nothing has been written to the FITS file for now
+        if (naxis1 == image->numCols && naxis2 == image->numRows) {
+            // We can write the whole lot at once
+            return psFitsWriteImage(fits, header, image, z, hdu->extname);
+        }
+        // Create a dummy image so we can write something larger than we actually have
+        psImage *dummy = psImageAlloc(naxis1, naxis2, image->type.type); // Dummy image
+        psImageInit(dummy, 0);
+        psImageOverlaySection(dummy, image, 0, 0, "=");
+        bool result = psFitsWriteImage(fits, header, dummy, z, hdu->extname);
+        psFree(dummy);
+        return result;
+    }
+
+    // We can simply update an existing HDU
+    if (hdu->blankPHU && !psFitsMoveExtNum(fits, 0, false)) {
+        psError(PS_ERR_IO, false, "Unable to move to PHU\n");
+        return false;
+    }
+    return psFitsUpdateImage(fits, image, readout->col0, readout->row0, z);
+}
+
+
+bool pmCellWrite(pmCell *cell, psFits *fits, psDB *db, bool blank)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return cellWrite(cell, fits, db, blank, FPA_WRITE_TYPE_IMAGE);
+}
+
+bool pmChipWrite(pmChip *chip, psFits *fits, psDB *db, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return chipWrite(chip, fits, db, blank, recurse, FPA_WRITE_TYPE_IMAGE);
+}
+
+bool pmFPAWrite(pmFPA *fpa, psFits *fits, psDB *db, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return fpaWrite(fpa, fits, db, blank, recurse, FPA_WRITE_TYPE_IMAGE);
+}
+
+
+bool pmCellWriteMask(pmCell *cell, psFits *fits, psDB *db, bool blank)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return cellWrite(cell, fits, db, blank, FPA_WRITE_TYPE_IMAGE);
+}
+
+bool pmChipWriteMask(pmChip *chip, psFits *fits, psDB *db, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return chipWrite(chip, fits, db, blank, recurse, FPA_WRITE_TYPE_MASK);
+}
+
+bool pmFPAWriteMask(pmFPA *fpa, psFits *fits, psDB *db, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return fpaWrite(fpa, fits, db, blank, recurse, FPA_WRITE_TYPE_MASK);
+}
+
+
+bool pmCellWriteWeight(pmCell *cell, psFits *fits, psDB *db, bool blank)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return cellWrite(cell, fits, db, blank, FPA_WRITE_TYPE_WEIGHT);
+}
+
+bool pmChipWriteWeight(pmChip *chip, psFits *fits, psDB *db, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return chipWrite(chip, fits, db, blank, recurse, FPA_WRITE_TYPE_WEIGHT);
+}
+
+bool pmFPAWriteWeight(pmFPA *fpa, psFits *fits, psDB *db, bool blank, bool recurse)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    return fpaWrite(fpa, fits, db, blank, recurse, FPA_WRITE_TYPE_WEIGHT);
+}
+
+
+int pmCellWriteTable(psFits *fits, const pmCell *cell, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, 0);
+    PS_ASSERT_PTR_NON_NULL(fits, 0);
+    PS_ASSERT_STRING_NON_EMPTY(name, 0);
+
+    const char *chipName = psMetadataLookupStr(NULL, cell->parent->concepts, "CHIP.NAME"); // Name of chip
+    const char *cellName = psMetadataLookupStr(NULL, cell->concepts, "CELL.NAME"); // Name of cell
+
+    psArray *table = psMetadataLookupPtr(NULL, cell->analysis, name); // The FITS table
+    if (!table) {
+        // We wrote everything we could find
+        return 0;
+    }
+
+    psString headerName = NULL;         // Name for header in analysis metadata
+    psStringAppend(&headerName, "%s.HEADER", name);
+    psMetadata *header = psMetadataLookupMetadata(NULL, cell->analysis, headerName); // The FITS header
+    psFree(headerName);
+
+    psString extname = NULL;            // Extension name
+    psStringAppend(&extname, "%s_%s_%s", name, chipName, cellName);
+
+    // XXX Could do a table lookup from the camera format, in case the input file isn't laid out with
+    // NAME_CHIP_CELL extension names --- use these as keys, and the value as the proper extension name.
+    // Allow interpolation of concepts, e.g., "{CHIP.NAME}" --> "ccd13".
+
+    if (!psFitsWriteTable(fits, header, table, extname)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to write table from chip %s, cell %s to extension %s\n",
+                chipName, cellName, extname);
+        psFree(extname);
+        return 0;
+    }
+
+    psFree(extname);
+    return 1;
+}
+
+
+int pmChipWriteTable(psFits *fits, const pmChip *chip, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, 0);
+    PS_ASSERT_PTR_NON_NULL(fits, 0);
+    PS_ASSERT_STRING_NON_EMPTY(name, 0);
+
+    int numWrite = 0;                    // Number of reads
+    psArray *cells = chip->cells;       // Array of cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // Cell of interest
+        numWrite += pmCellWriteTable(fits, cell, name);
+    }
+
+    return numWrite;
+}
+
+
+int pmFPAWriteTable(psFits *fits, const pmFPA *fpa, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, 0);
+    PS_ASSERT_PTR_NON_NULL(fits, 0);
+    PS_ASSERT_STRING_NON_EMPTY(name, 0);
+
+    int numWrite = 0;                    // Number of reads
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        numWrite += pmChipWriteTable(fits, chip, name);
+    }
+
+    return numWrite;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAWrite.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAWrite.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAWrite.h	(revision 15358)
@@ -0,0 +1,169 @@
+/* @file pmFPAWrite.h
+ * @brief Write FPA components to a FITS file
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.10 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-31 04:17:41 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_WRITE_H
+#define PM_FPA_WRITE_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Write a readout incrementally
+///
+/// Writes a readout to a FITS file, perhaps incrementally (if it has been read in using pmReadoutReadNext).
+/// Relies on the FITS header to specify how many image planes there are, and the width and height.
+bool pmReadoutWriteNext(pmReadout *readout, ///< Readout to write
+                        psFits *fits,   ///<  FITS file to which to write
+                        int z           ///<  Image plane to write
+                       );
+
+/// Write a cell to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU pixels if required.  A blank (i.e., image-less header) is
+/// written only if specifically requested.  Writes the concepts to the various locations, and then the HDU to
+/// the FITS file.  This function should be called at the beginning of the output cell loop with blank=true in
+/// order to produce the correct file structure.
+bool pmCellWrite(pmCell *cell,          ///<  Cell to write
+                 psFits *fits,          ///<  FITS file to which to write
+                 psDB *db,              ///<  Database handle for "concepts" update
+                 bool blank             ///<  Write a blank PHU?
+                );
+
+/// Write a chip to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU pixels if required.  A blank (i.e., image-less header) is
+/// written only if specifically requested.  Writes the concepts to the various locations, and then the HDU to
+/// the FITS file, optionally recursing to lower levels.  This function should be called at the beginning of
+/// the output chip loop with blank=true and recurse=false in order to produce the correct file structure.
+bool pmChipWrite(pmChip *chip,          ///<  Chip to write
+                 psFits *fits,          ///<  FITS file to which to write
+                 psDB *db,              ///<  Database handle for "concepts" update
+                 bool blank,            ///<  Write a blank PHU?
+                 bool recurse           ///<  Recurse to lower levels?
+                );
+
+/// Write an FPA to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU pixels if required.  A blank (i.e., image-less header) is
+/// written only if specifically requested.  Writes the concepts to the various locations, and then the HDU to
+/// the FITS file, optionally recursing to lower levels.  This function should be called at the beginning of
+/// the output FPA loop with blank=true and recurse=false in order to produce the correct file structure.
+bool pmFPAWrite(pmFPA *fpa,             ///<  FPA to write
+                psFits *fits,           ///<  FITS file to which to write
+                psDB *db,               ///<  Database handle for "concepts" update
+                bool blank,             ///<  Write a blank PHU?
+                bool recurse            ///<  Recurse to lower levels?
+               );
+
+/// Write a cell mask to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU mask pixels if required.  A blank (i.e., image-less
+/// header) is written only if specifically requested.  Writes the concepts to the various locations, and then
+/// the HDU mask to the FITS file.  This function should be called at the beginning of the output cell loop
+/// with blank=true in order to produce the correct file structure.
+bool pmCellWriteMask(pmCell *cell,      ///<  Cell to write
+                     psFits *fits,      ///<  FITS file to which to write
+                     psDB *db,          ///<  Database handle for "concepts" update
+                     bool blank         ///<  Write a blank PHU?
+                    );
+
+/// Write a chip mask to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU mask pixels if required.  A blank (i.e., image-less
+/// header) is written only if specifically requested.  Writes the concepts to the various locations, and then
+/// the HDU mask to the FITS file, optionally recursing to lower levels.  This function should be called at
+/// the beginning of the output chip loop with blank=true and recurse=false in order to produce the correct
+/// file structure.
+bool pmChipWriteMask(pmChip *chip,      ///<  Chip to write
+                     psFits *fits,      ///<  FITS file to which to write
+                     psDB *db,          ///<  Database handle for "concepts" update
+                     bool blank,        ///<  Write a blank PHU?
+                     bool recurse       ///<  Recurse to lower levels?
+                    );
+
+/// Write an FPA mask to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU mask pixels if required.  A blank (i.e., image-less
+/// header) is written only if specifically requested.  Writes the concepts to the various locations, and then
+/// the HDU mask to the FITS file, optionally recursing to lower levels.  This function should be called at
+/// the beginning of the output FPA loop with blank=true and recurse=false in order to produce the correct
+/// file structure.
+bool pmFPAWriteMask(pmFPA *fpa,         ///<  FPA to write
+                    psFits *fits,       ///<  FITS file to which to write
+                    psDB *db,           ///<  Database handle for "concepts" update
+                    bool blank,         ///<  Write a blank PHU?
+                    bool recurse        ///<  Recurse to lower levels?
+                   );
+
+/// Write a cell weight to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU weight pixels if required.  A blank (i.e., image-less
+/// header) is written only if specifically requested.  Writes the concepts to the various locations, and then
+/// the HDU weight to the FITS file.  This function should be called at the beginning of the output cell loop
+/// with blank=true in order to produce the correct file structure.
+bool pmCellWriteWeight(pmCell *cell,    ///<  Cell to write
+                       psFits *fits,    ///<  FITS file to which to write
+                       psDB *db,        ///<  Database handle for "concepts" update
+                       bool blank       ///<  Write a blank PHU?
+                      );
+
+/// Write a chip weight to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU weight pixels if required.  A blank (i.e., image-less
+/// header) is written only if specifically requested.  Writes the concepts to the various locations, and then
+/// the HDU weight to the FITS file, optionally recursing to lower levels.  This function should be called at
+/// the beginning of the output chip loop with blank=true and recurse=false in order to produce the correct
+/// file structure.
+bool pmChipWriteWeight(pmChip *chip,    ///<  Chip to write
+                       psFits *fits,    ///<  FITS file to which to write
+                       psDB *db,        ///<  Database handle for "concepts" update
+                       bool blank,      ///<  Write a blank PHU?
+                       bool recurse     ///<  Recurse to lower levels?
+                      );
+
+/// Write an FPA weight to a FITS file
+///
+/// Generates CELL.TRIMSEC, CELL.BIASSEC and the HDU weight pixels if required.  A blank (i.e., image-less
+/// header) is written only if specifically requested.  Writes the concepts to the various locations, and then
+/// the HDU weight to the FITS file, optionally recursing to lower levels.  This function should be called at
+/// the beginning of the output FPA loop with blank=true and recurse=false in order to produce the correct
+/// file structure.
+bool pmFPAWriteWeight(pmFPA *fpa,       ///<  FPA to write
+                      psFits *fits,     ///<  FITS file to which to write
+                      psDB *db,         ///<  Database handle for "concepts" update
+                      bool blank,       ///<  Write a blank PHU?
+                      bool recurse      ///<  Recurse to lower levels?
+                     );
+
+
+/// Write a FITS table from the cell's analysis metadata.
+///
+/// The FITS table (a psArray of psMetadatas) from the cell's analysis metadata (under "name") is written to
+/// the FITS file, at an extension specified by the name, chip name and cell name ("NAME_CHIP_CELL").  If a
+/// header is present in the analysis metadata ("name.HEADER"), then it is written also.
+int pmCellWriteTable(psFits *fits,      ///< FITS file to which to write
+                     const pmCell *cell, ///< Cell containing FITS table in the analysis metadata
+                     const char *name   ///< Name for the table data, and the extension name
+                    );
+
+int pmChipWriteTable(psFits *fits,      ///< FITS file to which to write
+                     const pmChip *chip, ///< Chip containing cells with tables to write
+                     const char *name   ///< Name for the table data, and the extension name
+                    );
+
+int pmFPAWriteTable(psFits *fits,       ///< FITS file to which to write
+                    const pmFPA *fpa,   ///< FPA containing cells with tables to write
+                    const char *name    ///< Name for the table data, and the extension name
+                   );
+
+// XXX better name, please
+bool pmFPAUpdateNames(pmFPA *fpa, pmChip *chip, pmCell *cell);
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPA_JPEG.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPA_JPEG.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPA_JPEG.c	(revision 15358)
@@ -0,0 +1,225 @@
+/** @file  pmFPA_JPEG.c
+ *
+ * This file contains functions to write JPEG images.
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.23 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:02:11 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+/*****************************************************************************/
+/* INCLUDE FILES                                                             */
+/*****************************************************************************/
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>            /* for strn?casecmp */
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPA_JPEG.h"
+
+
+bool pmFPAviewWriteJPEG(const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAWriteJPEG (fpa, view, file, config);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        pmChipWriteJPEG (chip, view, file, config);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmCellWriteJPEG (cell, view, file, config);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    pmReadoutWriteJPEG (readout, view, file, config);
+    return true;
+}
+
+// read in all chip-level JPEG files for this FPA
+bool pmFPAWriteJPEG (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+
+        pmChip *chip = fpa->chips->data[i];
+        pmChipWriteJPEG (chip, view, file, config);
+    }
+    return true;
+}
+
+// read in all cell-level JPEG files for this chip
+bool pmChipWriteJPEG (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    for (int i = 0; i < chip->cells->n; i++) {
+
+        pmCell *cell = chip->cells->data[i];
+        pmCellWriteJPEG (cell, view, file, config);
+    }
+    return true;
+}
+
+// read in all readout-level JPEG files for this cell
+bool pmCellWriteJPEG (pmCell *cell, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+
+        pmReadout *readout = cell->readouts->data[i];
+        pmReadoutWriteJPEG (readout, view, file, config);
+    }
+    return true;
+}
+
+// write JPEG image for readout
+// note that this function will try as hard a possible to write out a jpeg.  it will not fail
+// just because the values are in a poor range.  it is more convenient to know you are getting
+// a jpeg which is weird than to fail to get the file at all.
+bool pmReadoutWriteJPEG (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    bool status;
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    if (file->type != PM_FPA_FILE_JPEG) {
+        psError(PS_ERR_UNKNOWN, true, "warning: type mismatch");
+        return false;
+    }
+
+    psTrace("psModules.camera", 3, "writing jpeg for readout %zd\n", (size_t) readout);
+
+    psMetadata *recipe = psMetadataLookupMetadata(&status, config->recipes, "PPIMAGE");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "missing recipe PPIMAGE in config data");
+        return false;
+    }
+
+    psMetadata *options = psMetadataLookupMetadata(&status, recipe, file->name);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "missing %s options in PPIMAGE recipe", file->name);
+        return false;
+    }
+
+    float min = 0;
+    float max = 0;                 // Minimum and maximum for stretch
+    float mean = 0;
+    float delta = 0;
+
+    // measure the image statistics for scaling
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN);
+    stats->nSubsample = 10000;
+    if (!psImageBackground(stats, NULL, readout->image, NULL, 0, rng)) {
+        psStats *statsAlt = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+        stats->nSubsample = 10000;
+        if (!psImageBackground(stats, NULL, readout->image, NULL, 0, rng)) {
+            psLogMsg("jpeg", PS_LOG_WARN, "Unable to measure statistics for image, writing blank jpeg\n");
+            mean = 0;
+            delta = 1;
+        } else {
+            mean = statsAlt->sampleMean;
+            delta = 2*statsAlt->sampleStdev;
+        }
+        psFree(statsAlt);
+    } else {
+        mean = stats->robustMedian;
+        delta = stats->robustUQ - stats->robustLQ;
+    }
+    psFree(rng);
+    psFree(stats);
+
+    char *colormapName = psMemIncrRefCounter (psMetadataLookupStr (NULL, options, "COLORMAP"));
+    if (!colormapName)
+        colormapName = psStringCopy ("-greyscale");
+    char *mode = psMemIncrRefCounter (psMetadataLookupStr (NULL, options, "SCALE.MODE"));
+    if (!mode)
+        mode = psStringCopy ("RANGE");
+    float fmin = psMetadataLookupF32 (&status, options, "SCALE.MIN");
+    if (!status)
+        fmin = -3.0;
+    float fmax = psMetadataLookupF32 (&status, options, "SCALE.MAX");
+    if (!status)
+        fmax = +6.0;
+
+    if (!strcasecmp (mode, "RANGE")) {
+        min = mean + fmin*delta;
+        max = mean + fmax*delta;
+    }
+    if (!strcasecmp (mode, "FRACTION")) {
+        min = fmin*mean;
+        max = fmax*mean;
+    }
+    if (!strcasecmp (mode, "VALUE")) {
+        min = fmin;
+        max = fmax;
+    }
+
+    if (!isfinite(min) || !isfinite(max)) {
+        psLogMsg("jpeg", PS_LOG_WARN, "The stretch parameters are not both finite, writing blank jpeg\n");
+        min = 0;
+        max = 1;
+    }
+
+    psImageJpegColormap *map = psImageJpegColormapSet (NULL, colormapName);
+    if (!map) {
+        map = psImageJpegColormapSet (NULL, "-greyscale");
+    }
+
+    psImageJpeg (map, readout->image, file->filename, min, max);
+
+    psFree(map);
+    psFree(colormapName);
+    psFree(mode);
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPA_JPEG.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPA_JPEG.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPA_JPEG.h	(revision 15358)
@@ -0,0 +1,24 @@
+/* @file  pmFPAview.h
+ * @brief Tools to manipulate the FPA structure elements.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-04-14 03:22:47 $
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_JPEG_H
+#define PM_FPA_JPEG_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+bool pmFPAviewWriteJPEG (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmFPAWriteJPEG (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmChipWriteJPEG (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmCellWriteJPEG (pmCell *cell, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmReadoutWriteJPEG (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPA_MANAPLOT.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPA_MANAPLOT.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPA_MANAPLOT.c	(revision 15358)
@@ -0,0 +1,176 @@
+/** @file  pmFPA_MANAPLOT.c
+ *
+ * This file contains functions to write MANAPLOT images.
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-03-30 21:12:56 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+/*****************************************************************************/
+/* INCLUDE FILES                                                             */
+/*****************************************************************************/
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAfile.h"
+#include "pmFPAview.h"
+#include "pmFPA_MANAPLOT.h"
+
+
+bool pmFPAviewWriteMANAPLOT(const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAWriteMANAPLOT (fpa, view, file, config);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        pmChipWriteMANAPLOT (chip, view, file, config);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmCellWriteMANAPLOT (cell, view, file, config);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    pmReadoutWriteMANAPLOT (readout, view, file, config);
+    return true;
+}
+
+// read in all chip-level MANAPLOT files for this FPA
+bool pmFPAWriteMANAPLOT (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+
+        pmChip *chip = fpa->chips->data[i];
+        pmChipWriteMANAPLOT (chip, view, file, config);
+    }
+    return true;
+}
+
+// read in all cell-level MANAPLOT files for this chip
+bool pmChipWriteMANAPLOT (pmChip *chip, const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    for (int i = 0; i < chip->cells->n; i++) {
+
+        pmCell *cell = chip->cells->data[i];
+        pmCellWriteMANAPLOT (cell, view, file, config);
+    }
+    return true;
+}
+
+// read in all readout-level MANAPLOT files for this cell
+bool pmCellWriteMANAPLOT (pmCell *cell, const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+
+        pmReadout *readout = cell->readouts->data[i];
+        pmReadoutWriteMANAPLOT (readout, view, file, config);
+    }
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmReadoutWriteMANAPLOT (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    if (file->type != PM_FPA_FILE_MANAPLOT) {
+        psError(PS_ERR_UNKNOWN, true, "warning: type mismatch");
+        return false;
+    }
+
+    // XXX does not have to be a mana script...
+    // this function will call a mana script of the form:
+    // scriptname (input) (output)
+    // scriptname : extname
+    // input : filextra
+    // output : filerule
+
+    bool create = file->mode == PM_FPA_MODE_WRITE ? true : false;
+
+    psString tmpName;
+    tmpName = pmFPAfileNameFromRule (file->filextra, file, view);
+    psString input = pmConfigConvertFilename (tmpName, config, create);
+    psFree (tmpName);
+
+    tmpName = pmFPAfileNameFromRule (file->filerule, file, view);
+    psString output = pmConfigConvertFilename (tmpName, config, create);
+    psFree (tmpName);
+
+    tmpName = pmFPAfileNameFromRule (file->extname, file, view);
+    psString script = pmConfigConvertFilename (tmpName, config, create);
+    psFree (tmpName);
+
+    psString line = NULL;
+    psStringAppend (&line, "%s %s %s", script, input, output);
+
+    // capture the stdout and stderr?
+    // XXX use psPipe instead?
+    int status = system (line);
+    if (status == -1) {
+        psError(PS_ERR_UNKNOWN, true, "fork failure: %s", script);
+        return false;
+    }
+    if (WEXITSTATUS(status) != 0) {
+        psError(PS_ERR_UNKNOWN, true, "error running: %s", script);
+        return false;
+    }
+
+    psFree(line);
+    psFree(input);
+    psFree(output);
+    psFree(script);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPA_MANAPLOT.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPA_MANAPLOT.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPA_MANAPLOT.h	(revision 15358)
@@ -0,0 +1,24 @@
+/* @file  pmFPAview.h
+ * @brief Tools to manipulate the FPA structure elements.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:14 $
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_MANAPLOT_H
+#define PM_FPA_MANAPLOT_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+bool pmFPAviewWriteMANAPLOT (const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+bool pmFPAWriteMANAPLOT (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+bool pmChipWriteMANAPLOT (pmChip *chip, const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+bool pmCellWriteMANAPLOT (pmCell *cell, const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+bool pmReadoutWriteMANAPLOT (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAfile.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAfile.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAfile.c	(revision 15358)
@@ -0,0 +1,478 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>            /* for strn?casecmp */
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPACopy.h"
+#include "pmConcepts.h"
+
+static void pmFPAfileFree(pmFPAfile *file)
+{
+    if (!file) {
+        return;
+    }
+
+    psTrace ("pmFPAfileFree", 5, "freeing %s\n", file->name);
+    psFree (file->fpa);
+    psFree (file->src);
+    psFree (file->readout);
+    psFree (file->names);
+
+    psFree (file->camera);
+    psFree (file->cameraName);
+    psFree (file->format);
+    psFree (file->formatName);
+    psFree (file->name);
+
+    if (file->fits != NULL) {
+        psFitsClose (file->fits);
+    }
+    psFree(file->compression);
+
+    psFree (file->filerule);
+
+    psFree (file->filesrc);
+    psFree (file->detrend);
+
+    psFree (file->filename);
+    psFree (file->extname);
+
+    return;
+}
+
+pmFPAfile *pmFPAfileAlloc()
+{
+    pmFPAfile *file = psAlloc(sizeof(pmFPAfile));
+    psMemSetDeallocator(file, (psFreeFunc) pmFPAfileFree);
+
+    file->wrote_phu = false;
+    file->readout = NULL;
+    file->header = NULL;
+
+    file->fileLevel = PM_FPA_LEVEL_NONE;
+    file->dataLevel = PM_FPA_LEVEL_NONE;
+    file->freeLevel = PM_FPA_LEVEL_NONE;
+    file->mosaicLevel = PM_FPA_LEVEL_NONE;
+
+    file->type = PM_FPA_FILE_NONE;
+    file->mode = PM_FPA_MODE_NONE;
+    file->state = PM_FPA_STATE_CLOSED;
+
+    file->fpa = NULL;
+    file->fits = NULL;
+    file->compression = NULL;
+    file->bitpix = 0;
+    file->names = psMetadataAlloc();
+
+    file->camera = NULL;
+    file->cameraName = NULL;
+    file->format = NULL;
+    file->formatName = NULL;
+    file->name = NULL;
+
+    file->filerule = NULL;
+
+    file->filename = NULL;
+    file->extname  = NULL;
+
+    file->filesrc = NULL;
+    file->detrend = NULL;
+
+    file->xBin = 1;
+    file->yBin = 1;
+    file->src = NULL;
+
+    file->save = false;
+
+    return file;
+}
+
+// select the readout from the named pmFPAfile; if the named file does not exist,
+pmReadout *pmFPAfileThisReadout (psMetadata *files, const pmFPAview *view, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(files, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(name, false);
+    PS_ASSERT_INT_POSITIVE(strlen(name), false);
+
+    bool status;
+
+    pmFPAfile *file = psMetadataLookupPtr (&status, files, name);
+    if (file == NULL) {
+        return NULL;
+    }
+
+    // internal files have the readout as a separate element:
+    if (file->mode == PM_FPA_MODE_INTERNAL) {
+        return file->readout;
+    }
+
+    pmReadout *readout = pmFPAviewThisReadout (view, file->fpa);
+    return readout;
+}
+
+// select the cell from the named pmFPAfile; if the named file does not exist,
+pmCell *pmFPAfileThisCell (psMetadata *files, const pmFPAview *view, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(files, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(name, false);
+    PS_ASSERT_INT_POSITIVE(strlen(name), false);
+
+    bool status;
+
+    pmFPAfile *file = psMetadataLookupPtr (&status, files, name);
+    if (file == NULL) {
+        return NULL;
+    }
+
+    // internal files have the readout as a separate element:
+    if (file->mode == PM_FPA_MODE_INTERNAL) {
+        return NULL;
+    }
+
+    pmCell *cell = pmFPAviewThisCell(view, file->fpa);
+    return cell;
+}
+
+// select the readout from the named pmFPAfile; if the named file does not exist,
+pmChip *pmFPAfileThisChip (psMetadata *files, const pmFPAview *view, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(files, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(name, false);
+    PS_ASSERT_INT_POSITIVE(strlen(name), false);
+
+    bool status;
+
+    pmFPAfile *file = psMetadataLookupPtr (&status, files, name);
+    if (file == NULL) {
+        return NULL;
+    }
+
+    // internal files have the readout as a separate element:
+    if (file->mode == PM_FPA_MODE_INTERNAL) {
+        return NULL;
+    }
+
+    pmChip *chip = pmFPAviewThisChip (view, file->fpa);
+    return chip;
+}
+
+psString pmFPANameFromRule(const char *rule, const pmFPA *fpa, const pmFPAview *view)
+{
+    PS_ASSERT_STRING_NON_EMPTY(rule, NULL);
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+
+    psString newName = NULL;            // New name, to be returned
+    newName = psStringCopy(rule);
+
+    if (strstr (newName, "{FPA.NAME}") != NULL) {
+        char *name = psMetadataLookupStr (NULL, fpa->concepts, "FPA.NAME");
+        if (name != NULL) {
+            psStringSubstitute(&newName, "fpa", "{FPA.NAME}");
+        }
+    }
+    if (strstr (newName, "{CHIP.NAME}") != NULL) {
+        pmChip *chip = pmFPAviewThisChip (view, fpa);
+        if (chip != NULL) {
+            char *name = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+            if (name != NULL) {
+                psStringSubstitute(&newName, name, "{CHIP.NAME}");
+            }
+        }
+    }
+    if (strstr (newName, "{CHIP.ID}") != NULL) {
+        pmChip *chip = pmFPAviewThisChip (view, fpa);
+        if (chip != NULL) {
+            char *name = psMetadataLookupStr (NULL, chip->concepts, "CHIP.ID");
+            if (name != NULL) {
+                psStringSubstitute(&newName, name, "{CHIP.ID}");
+            }
+        }
+    }
+    if (strstr (newName, "{CHIP.N}") != NULL) {
+        char *name = NULL;
+        if (view->chip < 0) {
+            psStringAppend (&name, "XX");
+        } else {
+            psStringAppend (&name, "%02d", view->chip);
+        }
+        psStringSubstitute(&newName, name, "{CHIP.N}");
+        psFree (name);
+    }
+    if (strstr (newName, "{CELL.NAME}") != NULL) {
+        pmCell *cell = pmFPAviewThisCell (view, fpa);
+        if (cell != NULL) {
+            char *name = psMetadataLookupStr (NULL, cell->concepts, "CELL.NAME");
+            if (name != NULL) {
+                psStringSubstitute(&newName, name, "{CELL.NAME}");
+            }
+        }
+    }
+    if (strstr (newName, "{CELL.N}") != NULL) {
+        char *name = NULL;
+        if (view->cell < 0) {
+            psStringAppend (&name, "XX");
+        } else {
+            psStringAppend (&name, "%02d", view->cell);
+        }
+        psStringSubstitute(&newName, name, "{CELL.N}");
+    }
+    if (strstr (newName, "{EXTNAME}") != NULL) {
+        pmHDU *hdu = pmFPAviewThisHDU (view, fpa);
+        if (hdu->extname && *hdu->extname) {
+            psStringSubstitute(&newName, hdu->extname, "{EXTNAME}");
+        }
+    }
+    if (strstr (newName, "{FILTER}") != NULL) {
+        if (fpa != NULL) {
+            char *name = psMetadataLookupStr (NULL, fpa->concepts, "FPA.FILTER");
+            if (name && *name) {
+                psStringSubstitute(&newName, name, "{FILTER}");
+            }
+        }
+    }
+    if (strstr (newName, "{FILTER.ID}") != NULL) {
+        if (fpa != NULL) {
+            char *name = psMetadataLookupStr (NULL, fpa->concepts, "FPA.FILTERID");
+            if (name && *name) {
+                psStringSubstitute(&newName, name, "{FILTER.ID}");
+            }
+        }
+    }
+    if (strstr (newName, "{CAMERA}") != NULL) {
+        if (fpa != NULL) {
+            char *name = psMetadataLookupStr (NULL, fpa->concepts, "FPA.INSTRUMENT");
+            if (name && *name) {
+                psStringSubstitute(&newName, name, "{CAMERA}");
+            }
+        }
+    }
+    if (strstr (newName, "{INSTRUMENT}") != NULL) {
+        if (fpa != NULL) {
+            char *name = psMetadataLookupStr (NULL, fpa->concepts, "FPA.INSTRUMENT");
+            if (name && *name) {
+                psStringSubstitute(&newName, name, "{INSTRUMENT}");
+            }
+        }
+    }
+    if (strstr (newName, "{DETECTOR}") != NULL) {
+        if (fpa != NULL) {
+            char *name = psMetadataLookupStr (NULL, fpa->concepts, "FPA.DETECTOR");
+            if (name && *name) {
+                psStringSubstitute(&newName, name, "{DETECTOR}");
+            }
+        }
+    }
+    if (strstr (newName, "{TELESCOPE}") != NULL) {
+        if (fpa != NULL) {
+            char *name = psMetadataLookupStr (NULL, fpa->concepts, "FPA.TELESCOPE");
+            if (name && *name) {
+                psStringSubstitute(&newName, name, "{TELESCOPE}");
+            }
+        }
+    }
+    return newName;
+}
+
+// select the rule from the camera configuration, perform substitutions as needed
+psString pmFPAfileNameFromRule(const char *rule, const pmFPAfile *file, const pmFPAview *view)
+{
+    PS_ASSERT_PTR_NON_NULL(rule, NULL);
+    PS_ASSERT_INT_POSITIVE(strlen(rule), NULL);
+    PS_ASSERT_PTR_NON_NULL(file, NULL);
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+
+    psString newRule = NULL;            // Rule to pass on to pmFPANameFromRule
+    newRule = psStringCopy(rule);
+
+    if (strstr(newRule, "{OUTPUT}") != NULL) {
+        char *name = psMetadataLookupStr(NULL, file->names, "OUTPUT");
+        if (name) {
+            psStringSubstitute(&newRule, name, "{OUTPUT}");
+        }
+    }
+
+    psString newName = pmFPANameFromRule(newRule, file->fpa, view); // New name, to be returned
+    psFree(newRule);
+
+    return newName;
+}
+
+// given an already-opened fits file, write the components corresponding
+// to the specified view
+bool pmFPAfileCopyView (pmFPA *out, pmFPA *in, const pmFPAview *view)
+{
+    PS_ASSERT_PTR_NON_NULL(out, false);
+    PS_ASSERT_PTR_NON_NULL(in, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    // pmFPAWrite takes care of all PHUs as needed
+    if (view->chip == -1) {
+        pmFPACopy (out, in);
+        return true;
+    }
+    if (view->chip >= in->chips->n) {
+        psError(PS_ERR_IO, true, "Requested chip == %d >= in->chips->n == %ld", view->chip, in->chips->n);
+        return false;
+    }
+    pmChip *inChip = in->chips->data[view->chip];
+    pmChip *outChip = out->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        pmChipCopy (outChip, inChip);
+        return true;
+    }
+    if (view->cell >= inChip->cells->n) {
+        psError(PS_ERR_IO, true, "Requested cell == %d>= inChip->cells->n == %ld",
+                view->cell, inChip->cells->n);
+        return false;
+    }
+    pmCell *inCell = inChip->cells->data[view->cell];
+    pmCell *outCell = outChip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmCellCopy (outCell, inCell);
+        return true;
+    }
+    psError(PS_ERR_UNKNOWN, true, "Returning false");
+    return false;
+
+    // XXX add readout / segment equivalents
+}
+
+// given an already-opened fits file, write the components corresponding
+// to the specified view
+bool pmFPAfileCopyStructureView (pmFPA *out, pmFPA *in, int xBin, int yBin, const pmFPAview *view)
+{
+    bool status;
+    PS_ASSERT_PTR_NON_NULL(out, false);
+    PS_ASSERT_PTR_NON_NULL(in, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    // XXX this should be smarter (ie, only copy concepts from the current chips)
+    // but such a call is needed, so re-copy stuff rather than no copy
+    pmFPACopyConcepts (out, in);
+
+    // pmFPAWrite takes care of all PHUs as needed
+    if (view->chip == -1) {
+        status = pmFPACopyStructure (out, in, xBin, yBin);
+        return status;
+    }
+    if (view->chip >= in->chips->n) {
+        psError(PS_ERR_IO, true, "Requested chip == %d >= in->chips->n == %ld", view->chip, in->chips->n);
+        return false;
+    }
+    pmChip *inChip = in->chips->data[view->chip];
+    pmChip *outChip = out->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        status = pmChipCopyStructure (outChip, inChip, xBin, yBin);
+        return status;
+    }
+    if (view->cell >= inChip->cells->n) {
+        psError(PS_ERR_IO, true, "Requested cell == %d>= inChip->cells->n == %ld",
+                view->cell, inChip->cells->n);
+        return false;
+    }
+    pmCell *inCell = inChip->cells->data[view->cell];
+    pmCell *outCell = outChip->cells->data[view->cell];
+
+    status = pmCellCopyStructure (outCell, inCell, xBin, yBin);
+    return status;
+}
+
+pmFPAfileType pmFPAfileTypeFromString(const char *type)
+{
+    PS_ASSERT_STRING_NON_EMPTY(type, PM_FPA_FILE_NONE);
+
+    if (!strcasecmp (type, "SX"))     {
+        return PM_FPA_FILE_SX;
+    }
+    if (!strcasecmp (type, "OBJ"))     {
+        return PM_FPA_FILE_OBJ;
+    }
+    if (!strcasecmp (type, "CMP"))     {
+        return PM_FPA_FILE_CMP;
+    }
+    if (!strcasecmp (type, "CMF"))     {
+        return PM_FPA_FILE_CMF;
+    }
+    if (!strcasecmp (type, "RAW"))     {
+        return PM_FPA_FILE_RAW;
+    }
+    if (!strcasecmp (type, "IMAGE"))     {
+        return PM_FPA_FILE_IMAGE;
+    }
+    if (!strcasecmp (type, "PSF"))     {
+        return PM_FPA_FILE_PSF;
+    }
+    if (!strcasecmp (type, "JPEG"))     {
+        return PM_FPA_FILE_JPEG;
+    }
+    if (!strcasecmp (type, "KAPA"))     {
+        return PM_FPA_FILE_KAPA;
+    }
+    if (!strcasecmp (type, "MASK"))     {
+        return PM_FPA_FILE_MASK;
+    }
+    if (!strcasecmp (type, "WEIGHT"))     {
+        return PM_FPA_FILE_WEIGHT;
+    }
+    if (!strcasecmp (type, "FRINGE")) {
+        return PM_FPA_FILE_FRINGE;
+    }
+    if (!strcasecmp (type, "HEADER"))     {
+        return PM_FPA_FILE_HEADER;
+    }
+
+    return PM_FPA_FILE_NONE;
+}
+
+char *pmFPAfileStringFromType(pmFPAfileType type)
+{
+    switch (type) {
+      case PM_FPA_FILE_SX:
+        return ("SX");
+      case PM_FPA_FILE_OBJ:
+        return ("OBJ");
+      case PM_FPA_FILE_CMP:
+        return ("CMP");
+      case PM_FPA_FILE_CMF:
+        return ("CMF");
+      case PM_FPA_FILE_RAW:
+        return ("RAW");
+      case PM_FPA_FILE_IMAGE:
+        return ("IMAGE");
+      case PM_FPA_FILE_PSF:
+        return ("PSF");
+      case PM_FPA_FILE_JPEG:
+        return ("JPEG");
+      case PM_FPA_FILE_KAPA:
+        return ("KAPA");
+      case PM_FPA_FILE_MASK:
+        return ("MASK");
+      case PM_FPA_FILE_WEIGHT:
+        return ("WEIGHT");
+      case PM_FPA_FILE_FRINGE:
+        return ("FRINGE");
+      case PM_FPA_FILE_HEADER:
+        return ("HEADER");
+      default:
+        return ("NONE");
+    }
+    return ("NONE");
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAfile.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAfile.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAfile.h	(revision 15358)
@@ -0,0 +1,130 @@
+/* @file  pmFPAview.h
+ * @brief Tools to manipulate the FPA structure elements.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.22 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-10-03 21:32:43 $
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_FILE_H
+#define PM_FPA_FILE_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+typedef enum {
+    PM_FPA_BEFORE,
+    PM_FPA_AFTER,
+} pmFPAfilePlace;
+
+typedef enum {
+    PM_FPA_FILE_NONE,
+    PM_FPA_FILE_SX,
+    PM_FPA_FILE_OBJ,
+    PM_FPA_FILE_CMP,
+    PM_FPA_FILE_CMF,
+    PM_FPA_FILE_RAW,
+    PM_FPA_FILE_IMAGE,
+    PM_FPA_FILE_PSF,
+    PM_FPA_FILE_JPEG,
+    PM_FPA_FILE_KAPA,
+    PM_FPA_FILE_MASK,
+    PM_FPA_FILE_WEIGHT,
+    PM_FPA_FILE_FRINGE,
+    PM_FPA_FILE_HEADER,
+} pmFPAfileType;
+
+typedef enum {
+    PM_FPA_MODE_NONE,
+    PM_FPA_MODE_READ,
+    PM_FPA_MODE_WRITE,
+    PM_FPA_MODE_INTERNAL,
+    PM_FPA_MODE_REFERENCE,
+} pmFPAfileMode;
+
+typedef enum {
+    PM_FPA_STATE_OPEN     = 0x01,
+    PM_FPA_STATE_CLOSED   = 0x02,
+    PM_FPA_STATE_INACTIVE = 0x04,
+} pmFPAfileState;
+
+typedef struct {
+    pmFPAfileMode mode;                 // is this file read, written, or only used internally?
+    pmFPAfileType type;                 // what type of data is read from / written to disk?
+    pmFPAfileState state;               // have we opened the file, etc?
+
+    pmFPALevel fileLevel;               // what level in the FPA hierarchy represents a unique file?
+    pmFPALevel dataLevel;               // at what level do we read/write the data segment? (request by user)
+    pmFPALevel freeLevel;               // at what level do we free the data segment? (set by program)
+    pmFPALevel mosaicLevel;             // at what level is the mosaic?
+
+    pmFPA *fpa;                         // for I/O files, we carry a pointer to the complete fpa
+    psFits *fits;                       // for I/O files of fits type (IMAGE, CMP, CMF) we carry a file handle
+    psFitsCompression *compression;     // Compression for FITS images
+    int bitpix;                         // Bits per pixel for output
+
+    bool wrote_phu;                     // have we written a PHU for this file?
+    psMetadata *header;                 // pointer (view) to the current hdu header
+
+    pmReadout *readout;                 // for internal files, we only carry a single readout
+
+    psMetadata *names;                  // filenames supplied by the cmdline or detdb are saved here
+
+    char *filerule;                     // rule for constructing a filename when needed
+    char *filesrc;                      // rule to find file in pmFPAfile->names list
+
+    char *name;                         // the name of the rule (useful for debugging / tracing)
+    char *filename;                     // the current name of an active file
+    char *extname;                      // the current name of an active file extension
+
+    pmDetrendSelectResults *detrend;    // Detrend information, from pmDetrendSelect
+
+    bool save;                          // Should the file be saved?
+
+    // the following elements are used for WRITE-mode IMAGE-type pmFPAfiles to inform
+    // the creation of a new image based on an existing image
+    pmFPA *src;                         // if an output FPA, inherit from this FPA
+    int xBin;                           // desired binning in x direction
+    int yBin;                           // desired binning in y direction
+
+    psMetadata *camera;                 // Camera configuration
+    psString cameraName;                // Name of the camera
+    psMetadata *format;                 // Camera format
+    psString formatName;                // name of the camera format
+} pmFPAfile;
+
+// allocate an empty pmFPAfile structure
+pmFPAfile *pmFPAfileAlloc ();
+
+// select the readout from the named pmFPAfile; if the named file does not exist,
+pmReadout *pmFPAfileThisReadout (psMetadata *files, const pmFPAview *view, const char *name);
+
+// select the cell from the named pmFPAfile; if the named file does not exist,
+pmCell *pmFPAfileThisCell (psMetadata *files, const pmFPAview *view, const char *name);
+
+// select the chip from the named pmFPAfile; if the named file does not exist,
+pmChip *pmFPAfileThisChip (psMetadata *files, const pmFPAview *view, const char *name);
+
+// add the specified filename info (value) to the files of the given mode using the given reference name
+bool pmFPAfileAddFileNames (psMetadata *files, char *name, char *value, int mode);
+
+// convert the rule to a name based on the current view
+psString pmFPANameFromRule(const char *rule, const pmFPA *fpa, const pmFPAview *view);
+
+// convert the rule to a name based on the current view
+psString pmFPAfileNameFromRule(const char *rule, const pmFPAfile *file, const pmFPAview *view);
+
+bool pmFPAfileCopyView (pmFPA *out, pmFPA *in, const pmFPAview *view);
+
+bool pmFPAfileCopyStructureView (pmFPA *out, pmFPA *in, int xBin, int yBin, const pmFPAview *view);
+
+// Return the file type enum from a string
+pmFPAfileType pmFPAfileTypeFromString(const char *type);
+
+// Return the file type as a string
+char *pmFPAfileStringFromType(pmFPAfileType type);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileDefine.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileDefine.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileDefine.c	(revision 15358)
@@ -0,0 +1,1199 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>            /* for strn?casecmp */
+#include <pslib.h>
+
+#include "pmErrorCodes.h"
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAfile.h"
+#include "pmFPAConstruct.h"
+
+// Parse an option from a metadata, returning the appropriate integer value
+static int parseOptionInt(const psMetadata *md, // Metadata containing the option
+                          const char *name, // Option name
+                          const char *source, // Description of source, for warning messages
+                          int defaultValue // Default value
+                          )
+{
+    psMetadataItem *item = psMetadataLookup(md, name); // Item with the value of interest
+    if (!item) {
+        psWarning("Unable to find value for %s in %s --- set to %d.", name, source, defaultValue);
+        return defaultValue;
+    }
+    int value = psMetadataItemParseS32(item); // Value of interst
+    return value;
+}
+
+
+// define an input-type pmFPAfile, bind to the optional fpa if supplied
+pmFPAfile *pmFPAfileDefineInput(const pmConfig *config, pmFPA *fpa, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(config->files, NULL);
+    PS_ASSERT_PTR_NON_NULL(config->camera, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    bool status;
+    char *type;
+
+    const psMetadata *camera = (fpa ? fpa->camera : config->camera); // Camera configuration for this file
+    psMetadata *data = pmConfigFileRule(config, camera, name); // File rule
+    if (!data) {
+        psError(PS_ERR_IO, true, "Can't find file rule %s!", name);
+        return NULL;
+    }
+
+    pmFPAfile *file = pmFPAfileAlloc ();
+
+    // save the name of this pmFPAfile
+    file->name = psStringCopy (name);
+
+    file->filerule = psMemIncrRefCounter(psMetadataLookupStr (&status, data, "FILENAME.RULE"));
+
+    type = psMetadataLookupStr (&status, data, "FILE.TYPE");
+    file->type = pmFPAfileTypeFromString(type);
+    if (file->type == PM_FPA_FILE_NONE) {
+        psError(PS_ERR_IO, true, "FILE.TYPE is not defined for %s\n", name);
+        return NULL;
+    }
+
+    file->mode = PM_FPA_MODE_READ;
+    file->fileLevel = PM_FPA_LEVEL_NONE; // the fileLevel depends on the input data
+
+    file->dataLevel = pmFPALevelFromName(psMetadataLookupStr (&status, data, "DATA.LEVEL"));
+    if (file->dataLevel == PM_FPA_LEVEL_NONE) {
+        psError(PS_ERR_IO, true, "DATA.LEVEL is not set for %s\n", name);
+        return NULL;
+    }
+    // default is to free the data after use (after written out)
+    // this can be overridden for pmFPAfiles used as carriers as well
+    file->freeLevel = file->dataLevel;
+
+    if (fpa) {
+        file->fpa = psMemIncrRefCounter(fpa);
+        file->camera = psMemIncrRefCounter((psMetadata *)fpa->camera);
+        file->cameraName = psMemIncrRefCounter(config->cameraName); // XXX Is this the correct thing to do?
+    } else {
+        file->camera = psMemIncrRefCounter(config->camera);
+        file->cameraName = psMemIncrRefCounter(config->cameraName);
+    }
+
+    // XXX ppImage and similar require the added file to be unique
+    // XXX ppFocus wants to override the selection with the new selection
+    // XXX require programs like ppFocus to remove existing files by hand
+    if (!psMetadataAddPtr (config->files, PS_LIST_TAIL, name,
+                           PS_DATA_UNKNOWN | PS_META_DUPLICATE_OK, "", file)) {
+        psError (PS_ERR_IO, false, "could not add %s to config files", name);
+        return NULL;
+    }
+    psFree (file);
+    return (file);
+}
+
+// Define an output pmFPAfile
+pmFPAfile *pmFPAfileDefineOutputForFormat(const pmConfig *config, // Configuration
+                                          pmFPA *fpa, // Optional FPA to bind
+                                          const char *name, // Name of file rule
+                                          psString cameraName, // Name of camera configuration to use
+                                          psString formatName // Name of camera format to use
+    )
+{
+    bool status;
+
+    // Use the camera we were told to, the camera of the provided FPA, or default to the default camera
+    psMetadata *camera;                 // Camera configuration
+    if (!cameraName || strlen(cameraName) == 0) {
+        if (fpa && fpa->camera) {
+            camera = (psMetadata*)fpa->camera; // Casting away const, so I can put it in the file
+        } else {
+            camera = config->camera;
+            cameraName = config->cameraName;
+        }
+    } else {
+        bool mdok;                      // Status of MD lookup
+        psMetadata *cameras = psMetadataLookupMetadata(&mdok, config->site, "CAMERAS"); // Known cameras
+        if (!mdok || !cameras) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the site configuration.\n");
+            return NULL;
+        }
+        camera = psMetadataLookupMetadata(&mdok, cameras, cameraName); // Camera configuration of interest
+        if (!mdok || !camera) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find automatically generated "
+                    "camera configuration %s in site configuration.\n", cameraName);
+            return NULL;
+        }
+
+        if (fpa && fpa->camera && fpa->camera != camera) {
+            psAbort("Camera of bound FPA is not the requested camera --- there is an inconsistency!");
+        }
+    }
+
+    psMetadata *data = pmConfigFileRule(config, camera, name); // File rule
+    if (!data) {
+        psError(PS_ERR_IO, true, "Can't find file rule %s!", name);
+        return NULL;
+    }
+
+    pmFPAfile *file = pmFPAfileAlloc();
+
+    // save the name of this pmFPAfile
+    file->name = psStringCopy(name);
+
+    file->filerule = psMemIncrRefCounter(psMetadataLookupStr(&status, data, "FILENAME.RULE"));
+
+    const char *type = psMetadataLookupStr(&status, data, "FILE.TYPE");
+    file->type = pmFPAfileTypeFromString(type);
+    if (file->type == PM_FPA_FILE_NONE) {
+        psError(PS_ERR_IO, true, "FILE.TYPE is not defined for %s\n", name);
+        psFree(file);
+        return NULL;
+    }
+
+    file->mode = PM_FPA_MODE_WRITE;
+    file->save = false;
+
+    file->camera = psMemIncrRefCounter(camera);
+    file->cameraName = psMemIncrRefCounter(cameraName);
+
+    // XXX this seems a bit of a hack: use the cameraName to determine the mosaic level...
+    # if (0)
+    if (cameraName) {
+        if (!strcmp(cameraName + strlen(cameraName) - 5, "-CHIP")) {
+            file->mosaicLevel = PM_FPA_LEVEL_CHIP;
+        }
+        if (!strcmp(cameraName + strlen(cameraName) - 5, "-FPA")) {
+            file->mosaicLevel = PM_FPA_LEVEL_FPA;
+        }
+    }
+    # endif
+
+    // Use the format we were told to, the format specified in the file rule, or default to the default format
+    if (!formatName || strlen(formatName) == 0) {
+        // select the format list from the selected camera
+        formatName = psMetadataLookupStr(&status, data, "FILE.FORMAT");
+        if (!formatName || strcmp(formatName, "NONE") == 0) {
+            // Try to get by with the default
+            formatName = config->formatName;
+        }
+    }
+    psMetadata *formats = psMetadataLookupMetadata(&status, file->camera, "FORMATS"); // List of formats
+    psMetadata *format = psMetadataLookupMetadata(&status, formats, formatName); // Camera format to use
+    if (!format) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to find format %s for file %s.\n",
+                formatName, file->name);
+        psFree(file);
+        return NULL;
+    }
+    file->format = psMemIncrRefCounter(format);
+    file->formatName = psStringCopy(formatName);
+
+    if (fpa) {
+        file->fpa = psMemIncrRefCounter(fpa);
+    } else {
+        file->fpa = pmFPAConstruct(file->camera);
+    }
+
+    // Get FITS output scheme
+    const char *fitsType = psMetadataLookupStr(&status, data, "FITS.TYPE"); // Name of FITS scheme to use
+    if (fitsType && strcasecmp(fitsType, "NONE") != 0) {
+        psMetadata *fitsTypes = psMetadataLookupMetadata(&status, camera, "FITS"); // The FITS schemes
+        if (!fitsTypes) {
+            psWarning("Unable to find FITS in camera configuration --- compression disabled.");
+            goto COMPRESSION_DONE;
+        }
+        psMetadata *scheme = psMetadataLookupMetadata(NULL, fitsTypes, fitsType); // FITS scheme
+        if (!scheme) {
+            psWarning("Unable to find %s in FITS in camera configuration --- compression disabled.",
+                      fitsType);
+            goto COMPRESSION_DONE;
+        }
+        const char *typeString = psMetadataLookupStr(NULL, scheme, "COMPRESSION"); // Compression type
+        if (!typeString || strlen(typeString) == 0) {
+            psWarning("Can't find COMPRESSION in FITS scheme %s --- compression disabled.", fitsType);
+            goto COMPRESSION_DONE;
+        }
+        psFitsCompressionType type = psFitsCompressionTypeFromString(typeString); // Compression type enum
+
+        psString source = NULL;         // Source of options
+        psStringAppend(&source, "%s in FITS in camera %s", fitsType, cameraName);
+        psVector *tile = psVectorAlloc(3, PS_TYPE_S32); // Tile sizes
+        file->bitpix = parseOptionInt(scheme, "BITPIX", source, 0); // Bits per pixel
+        tile->data.S32[0] = parseOptionInt(scheme, "TILE.X", source, 0); // Tiling in x
+        tile->data.S32[1] = parseOptionInt(scheme, "TILE.Y", source, 1); // Tiling in y
+        tile->data.S32[2] = parseOptionInt(scheme, "TILE.Z", source, 1); // Tiling in z
+        int noise = parseOptionInt(scheme, "NOISE", source, 16); // Noise bits
+        int hscale = parseOptionInt(scheme, "HSCALE", source, 0); // Scaling for HCOMPRESS
+        int hsmooth = parseOptionInt(scheme, "HSMOOTH", source, 0); // Smoothing for HCOMPRESS
+        psFree(source);
+
+        file->compression = psFitsCompressionAlloc(type, tile, noise, hscale, hsmooth);
+        psFree(tile);
+    }
+COMPRESSION_DONE:
+
+    file->fileLevel = pmFPAPHULevel(format);
+    if (file->fileLevel == PM_FPA_LEVEL_NONE) {
+        psError(PS_ERR_IO, true, "Unable to determine file level for %s\n", name);
+        psFree(file);
+        return NULL;
+    }
+
+    file->dataLevel = pmFPALevelFromName(psMetadataLookupStr(&status, data, "DATA.LEVEL"));
+    if (file->dataLevel == PM_FPA_LEVEL_NONE) {
+        psError(PS_ERR_IO, true, "DATA.LEVEL is not set for %s\n", name);
+        psFree(file);
+        return NULL;
+    }
+    // default is to free the data after use (after written out)
+    // this can be overridden for pmFPAfiles used as carriers as well
+    file->freeLevel = file->dataLevel;
+    file->fileLevel = PS_MIN (file->fileLevel, file->dataLevel);
+
+    // XXX the file/data/free level must be consistent with the reference fpa (but since we
+    // don't have access to its pmFPAfile, we cannot enforce this here...
+
+    pmFPALevel extLevel = pmFPAExtensionsLevel(format); // Level for extensions
+    if (extLevel != PM_FPA_LEVEL_NONE) {
+        if (extLevel < file->dataLevel) {
+            psWarning("Level for extensions is higher than desired data level --- adjusting.\n");
+            file->dataLevel = extLevel;
+        }
+        if (extLevel < file->freeLevel) {
+            psWarning("Level for extensions is higher than desired free level --- adjusting.\n");
+            file->freeLevel = extLevel;
+        }
+    } else {
+        // if we do not have extensions in the file, we are forced to write out at the file level
+        file->dataLevel = file->fileLevel;
+        file->freeLevel = file->fileLevel;
+    }
+
+    psTrace ("psModules.camera", 5, "file: %s, format: %s, fileLevel: %s, extLevel: %s, dataLevel: %s, freeLevel: %s\n",
+             file->name, file->formatName, pmFPALevelToName (file->fileLevel), pmFPALevelToName(extLevel), pmFPALevelToName (file->dataLevel), pmFPALevelToName (file->freeLevel));
+
+    // add argument-supplied OUTPUT name to this file
+    char *outname = psMetadataLookupStr(&status, config->arguments, "OUTPUT");
+    psMetadataAddStr(file->names, PS_LIST_TAIL, "OUTPUT", PS_META_NO_REPLACE, "", outname);
+
+    // place the resulting file in the config system
+    psMetadataAddPtr (config->files, PS_LIST_TAIL, name, PS_DATA_UNKNOWN, "", file);
+    psFree (file); // we free this copy of file, but 'files' still has a copy
+    return (file); // the returned value is a view into the version on 'files'
+}
+
+// define a pmFPAfile, bind to the optional fpa if supplied
+pmFPAfile *pmFPAfileDefineOutput(const pmConfig *config, pmFPA *fpa, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(config->files, NULL);
+    PS_ASSERT_PTR_NON_NULL(config->camera, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    return pmFPAfileDefineOutputForFormat(config, fpa, name, NULL, NULL);
+}
+
+// define a pmFPAfile, bind to the optional file if supplied
+pmFPAfile *pmFPAfileDefineOutputFromFile(const pmConfig *config, pmFPAfile *file, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(config->files, NULL);
+    PS_ASSERT_PTR_NON_NULL(config->camera, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    char *cameraName = NULL, *formatName = NULL; // Name of camera and format
+    pmFPA *fpa = NULL;                  // FPA for file
+    if (file) {
+        cameraName = file->cameraName;
+        formatName = file->formatName;
+        fpa = file->fpa;
+    }
+
+    return pmFPAfileDefineOutputForFormat(config, fpa, name, cameraName, formatName);
+}
+
+// search for argname on the config->argument list
+// construct an FPA based on the files in this list (must represent a single FPA)
+// built the association between the FPA elements (CHIP/CELL) and the files
+// define the pmFPAfile filename and bind it to this FPA
+// save the pmFPAfile on config->files
+// return the pmFPAfile (a view to the one saved on config->files)
+pmFPAfile *pmFPAfileDefineFromArgs(bool *success, pmConfig *config, const char *filename, const char *argname)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(argname, NULL);
+
+    bool status;
+    pmFPA *fpa = NULL;
+    psFits *fits = NULL;
+    pmFPAfile *file = NULL;
+    psMetadata *phu = NULL;
+    psMetadata *format = NULL;
+
+    // use success to identify valid exit conditions (as opposed to 'argument not supplied')
+    if (success) *success = false;
+
+    // we search the argument data for the named fileset (argname)
+    psArray *infiles = psMetadataLookupPtr(&status, config->arguments, argname);
+    if (!status) {
+        if (success) *success = true;
+        return NULL;
+    }
+    if (infiles->n < 1) {
+        psError(PS_ERR_IO, false, "Found n == %ld files in %s in arguments\n", infiles->n, argname);
+        return NULL;
+    }
+
+    // this function is implicitly an INPUT operation: do not create the file
+    psString realName = pmConfigConvertFilename (infiles->data[0], config, false);
+    if (!realName) {
+        psError(PS_ERR_IO, false, "Failed to convert file name %s\n", (char *) infiles->data[0]);
+        return NULL;
+    }
+
+    // load the header of the first image
+    // EXTWORD (fits->extword) is not relevant to the PHU
+    fits = psFitsOpen (realName, "r");
+    if (!fits) {
+        psError(PS_ERR_IO, false, "Failed to open file %s\n", realName);
+        psFree (realName);
+        return NULL;
+    }
+    phu = psFitsReadHeader (NULL, fits);
+    if (!phu) {
+        psError(PS_ERR_IO, false, "Failed to read file header %s\n", realName);
+        psFree (realName);
+        return NULL;
+    }
+    psFitsClose(fits);
+
+    // determine the current format from the header
+    // determine camera if not specified already
+    format = pmConfigCameraFormatFromHeader (config, phu, true);
+    if (!format) {
+        psError(PS_ERR_IO, false, "Failed to read CCD format from %s\n", realName);
+        psFree(phu);
+        psFree (realName);
+        return NULL;
+    }
+
+    // build the template fpa, set up the basic view
+    fpa = pmFPAConstruct (config->camera);
+    if (!fpa) {
+        psError(PS_ERR_IO, false, "Failed to construct FPA from %s", realName);
+        psFree (realName);
+        return NULL;
+    }
+    psFree (realName);
+
+    // load the given filerule (from config->camera) and bind it to the fpa
+    // the returned file is just a view to the entry on config->files
+    file = pmFPAfileDefineInput (config, fpa, filename);
+    if (!file) {
+        psError(PS_ERR_IO, false, "file %s not defined\n", filename);
+        psFree(phu);
+        psFree(fpa);
+        psFree(format);
+        return NULL;
+    }
+    psFree (format);
+    file->format = psMemIncrRefCounter(format);
+    file->formatName = psStringCopy(config->formatName);
+
+    // adjust the rules to identify these files in the file->names data
+    psFree (file->filerule);
+    file->filerule = psStringCopy ("@FILES");
+    file->filesrc = psStringCopy ("{CHIP.NAME}.{CELL.NAME}");
+
+    file->fileLevel = pmFPAPHULevel(format);
+    if (file->fileLevel == PM_FPA_LEVEL_NONE) {
+        psError(PS_ERR_IO, true, "Unable to determine file level for %s\n", file->name);
+        psFree(phu);
+        psFree(fpa);
+        psFree(format);
+        psFree(file);
+        return NULL;
+    }
+
+    // examine the list of input files and validate their cameras
+    // associated each filename with an element of the FPA
+    // save the association on file->names
+    for (int i = 0; i < infiles->n; i++) {
+        if (i > 0) {
+            // this function is implicitly an INPUT operation: do not create the file
+            psString realName = pmConfigConvertFilename (infiles->data[i], config, false);
+            if (!realName) {
+                psError(PS_ERR_IO, false, "Failed to convert file name %s", (char *) infiles->data[i]);
+                return NULL;
+            }
+            fits = psFitsOpen (realName, "r");
+            if (!fits) {
+                psError(PS_ERR_IO, false, "Failed to open file %s\n", realName);
+                psFree (realName);
+                return NULL;
+            }
+            phu = psFitsReadHeader (NULL, fits);
+            if (!phu) {
+                psError(PS_ERR_IO, false, "Failed to read file header %s", realName);
+                psFree (realName);
+                psFitsClose (fits);
+                return NULL;
+            }
+            bool valid = false;
+            if (!pmConfigValidateCameraFormat (&valid, format, phu)) {
+                psError (PS_ERR_UNKNOWN, false, "Error in config scripts\n");
+                psFree (realName);
+                psFitsClose (fits);
+                return NULL;
+            }
+            if (!valid) {
+                psError(PS_ERR_IO, false, "file %s is not from the required camera", realName);
+                psFree (realName);
+                psFitsClose (fits);
+                return NULL;
+            }
+            psFree(realName);
+            psFitsClose (fits);
+        }
+
+        // set the view to the corresponding entry for this phu
+        pmFPAview *view = pmFPAAddSourceFromHeader (fpa, phu, format);
+        if (!view) {
+            psError(PS_ERR_IO, false, "Unable to determine source for %s", file->name);
+            psFree(phu);
+            psFree (fpa);
+            psFree (format);
+            return NULL;
+        }
+
+        // associate the filename with the FPA element
+        char *name = pmFPAfileNameFromRule (file->filesrc, file, view);
+
+        // save the name association in the pmFPAfile structure
+        psMetadataAddStr (file->names, PS_LIST_TAIL, name, 0, "", infiles->data[i]);
+
+        psFree (view);
+        psFree (name);
+        psFree (phu);
+    }
+    psFree (fpa);
+    if (success) *success = true;
+
+    return file;
+}
+
+// search for argname on the config->argument list
+// construct an FPA based on the files in this list (must represent a single FPA)
+// built the association between the FPA elements (CHIP/CELL) and the files
+// define the pmFPAfile filename and bind it to this FPA
+// save the pmFPAfile on config->files
+// return the pmFPAfile (a view to the one saved on config->files)
+pmFPAfile *pmFPAfileBindFromArgs (bool *success, pmFPAfile *input, pmConfig *config, const char *filename, const char *argname)
+{
+    PS_ASSERT_PTR_NON_NULL(input, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(argname, NULL);
+
+    bool status;
+    psFits *fits = NULL;
+    pmFPAfile *file = NULL;
+    psMetadata *phu = NULL;
+
+    // use success to identify valid exit conditions (as opposed to 'argument not supplied')
+    if (success) *success = false;
+
+    // we search the argument data for the named fileset (argname)
+    psArray *infiles = psMetadataLookupPtr(&status, config->arguments, argname);
+    if (!status) {
+        // this is not an error: this just means no matching argument was supplied
+        if (success) *success = true;
+        return NULL;
+    }
+    if (infiles->n < 1) {
+        psError(PS_ERR_IO, false, "Found n == %ld files in %s in arguments\n", infiles->n, argname);
+        return NULL;
+    }
+
+    // load the given filerule (from config->camera) and bind it to the fpa
+    // the returned file is just a view to the entry on config->files
+    file = pmFPAfileDefineInput (config, input->fpa, filename);
+    if (!file) {
+        psError(PS_ERR_IO, false, "file %s not defined\n", filename);
+        psFree(phu);
+        return NULL;
+    }
+
+    // set derived values
+    file->fileLevel = input->fileLevel;
+
+    // define the rule to identify these files in the file->names data
+    psFree (file->filerule);
+    psFree (file->filesrc);
+    file->filerule = psStringCopy ("@FILES");
+    file->filesrc = psStringCopy ("{CHIP.NAME}.{CELL.NAME}");
+
+    // examine the list of input files and validate their cameras
+    // associated each filename with an element of the FPA
+    // save the association on file->names
+    psMetadata *format = NULL;
+    for (int i = 0; i < infiles->n; i++) {
+        // this function is implicitly an INPUT operation: do not create the file
+        psString realName = pmConfigConvertFilename (infiles->data[i], config, false);
+        if (!realName) {
+            psError(PS_ERR_IO, false, "Failed to convert file name %s", (char *) infiles->data[i]);
+            return NULL;
+        }
+        fits = psFitsOpen (realName, "r");
+        if (!fits) {
+            psError(PS_ERR_IO, false, "Failed to open file %s\n", realName);
+            psFree (realName);
+            return NULL;
+        }
+        phu = psFitsReadHeader (NULL, fits);
+        if (!phu) {
+            psError(PS_ERR_IO, false, "Failed to read file header %s", realName);
+            psFree (realName);
+            psFitsClose (fits);
+            return NULL;
+        }
+
+        if (!format) {
+            format = pmConfigCameraFormatFromHeader(config, phu, true);
+            if (!format) {
+                psError(PS_ERR_IO, false, "Failed to read CCD format from %s\n", realName);
+                psFree(phu);
+                psFree(realName);
+                psFitsClose(fits);
+                psFree(file);
+                return NULL;
+            }
+        } else {
+            bool valid = false;
+            if (!pmConfigValidateCameraFormat(&valid, format, phu)) {
+                psError(PS_ERR_UNKNOWN, false, "Error in config scripts\n");
+                psFree(realName);
+                psFree(file);
+                psFitsClose(fits);
+                return NULL;
+            }
+            if (!valid) {
+                psError(PS_ERR_IO, false, "specified data file %s does not match format of supplied INPUT\n",
+                        realName);
+                psFree(realName);
+                psFree(file);
+                psFitsClose(fits);
+                return NULL;
+            }
+        }
+
+        psFree(realName);
+        psFitsClose(fits);
+
+        // set the view to the corresponding entry for this phu
+        pmFPAview *view = pmFPAIdentifySourceFromHeader (input->fpa, phu, format);
+        if (!view) {
+            psError(PS_ERR_IO, false, "Unable to determine source for %s", file->name);
+            psFree(phu);
+            return NULL;
+        }
+
+        // associate the filename with the FPA element
+        char *name = pmFPAfileNameFromRule(file->filesrc, file, view);
+
+        // save the name association in the pmFPAfile structure
+        psMetadataAddStr (file->names, PS_LIST_TAIL, name, 0, "", infiles->data[i]);
+
+        psFree (view);
+        psFree (name);
+        psFree (phu);
+    }
+    psFree(file->format);
+    file->format = format;
+    file->formatName = psStringCopy(config->formatName);
+
+    if (success) *success = true;
+    return file;
+}
+
+// search for argname on the config->argument list
+// construct an FPA based on the files in this list (each represents the same FPA)
+// built the association between the FPA elements (CHIP/CELL) and the files
+// define the pmFPAfile filenames and bind them to the FPAs
+// save the pmFPAfiles on config->files
+// return the pmFPAfiles (a view to the one saved on config->files)
+pmFPAfile *pmFPAfileDefineSingleFromArgs (bool *success, pmConfig *config, const char *filename,
+        const char *argname, int entry)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(argname, NULL);
+
+    bool status;
+    pmFPA *fpa = NULL;
+    psFits *fits = NULL;
+    pmFPAfile *file = NULL;
+    psMetadata *phu = NULL;
+    psMetadata *format = NULL;
+
+    if (success) *success = false;
+
+    // we search the argument data for the named fileset (argname)
+    psArray *infiles = psMetadataLookupPtr(&status, config->arguments, argname);
+    if (!status) {
+        psTrace("psModules.camera", 5, "Failed to find %s in argument list", argname);
+        if (success) *success = true;
+        return NULL;
+    }
+    if (infiles->n <= entry) {
+        psError(PS_ERR_IO, false, "only %ld files in %s in argument, entry %d requested\n", infiles->n, argname, entry);
+        return NULL;
+    }
+
+    // examine the list of input files and validate their cameras
+    // associated each filename with an element of the FPA
+    // save the association on file->names
+    fits = psFitsOpen (infiles->data[entry], "r");
+    phu = psFitsReadHeader (NULL, fits);
+    psFitsClose (fits);
+
+    // on first call to this function, config->camera is not set.
+    // later calls will give an error if the cameras do not match
+    format = pmConfigCameraFormatFromHeader (config, phu, true);
+    if (!format) {
+        psError(PS_ERR_IO, false, "Failed to read CCD format from %s\n", (char *)infiles->data[0]);
+        psFree(phu);
+        return NULL;
+    }
+
+    // build the template fpa, set up the basic view
+    fpa = pmFPAConstruct (config->camera);
+    if (!fpa) {
+        psError(PS_ERR_IO, false, "Failed to construct FPA from %s", (char *)infiles->data[0]);
+        psFree(phu);
+        psFree(format);
+        return NULL;
+    }
+
+    // load the given filerule (from config->camera) and bind it to the fpa
+    // the returned file is just a view to the entry on config->files
+    // we need a variable name here... (but in filerule)
+    file = pmFPAfileDefineInput (config, fpa, filename);
+    if (!file) {
+        psError(PS_ERR_IO, false, "file %s not defined\n", filename);
+        psFree(phu);
+        psFree(fpa);
+        psFree(format);
+        return NULL;
+    }
+    psFree (file->format);
+    file->format = format;
+    file->formatName = psStringCopy(config->formatName);
+
+    // adjust the rules to identify these files in the file->names data
+    psFree (file->filerule);
+    psFree (file->filesrc);
+    file->filerule = psStringCopy ("@FILES");
+    file->filesrc = psStringCopy ("{CHIP.NAME}.{CELL.NAME}");
+
+    file->fileLevel = pmFPAPHULevel(format);
+    if (file->fileLevel == PM_FPA_LEVEL_NONE) {
+        psError(PS_ERR_IO, true, "Unable to determine file level for %s\n", file->name);
+        psFree(phu);
+        psFree(fpa);
+        psFree(file);
+        psFree(format);
+        return NULL;
+    }
+
+    // set the view to the corresponding entry for this phu
+    pmFPAview *view = pmFPAAddSourceFromHeader (fpa, phu, format);
+    if (!view) {
+        psError(PS_ERR_IO, false, "Unable to determine source for %s", file->name);
+        psFree(phu);
+        psFree(fpa);
+        psFree(file);
+        psFree(format);
+        return NULL;
+    }
+
+    // associate the filename with the FPA element
+    char *name = pmFPAfileNameFromRule (file->filesrc, file, view);
+
+    // save the name association in the pmFPAfile structure
+    psMetadataAddStr (file->names, PS_LIST_TAIL, name, 0, "", infiles->data[entry]);
+
+    psFree(phu);
+    psFree(fpa);
+    psFree(view);
+    psFree(name);
+    psFree(format);
+
+    if (success) *success = true;
+    return file;
+}
+
+// define the named pmFPAfile from the camera->config
+// only valid for pmFPAfile->mode = READ
+pmFPAfile *pmFPAfileDefineFromConf (bool *success, const pmConfig *config, const char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+
+    if (success) *success = false;
+
+    // a camera config is needed (as source of file rule)
+    if (config->camera == NULL) {
+        psError(PS_ERR_IO, true, "camera is not defined");
+        return NULL;
+    }
+
+    // build the template fpa, set up the basic view
+    pmFPA *fpa = pmFPAConstruct (config->camera);
+    if (!fpa) {
+        psError(PS_ERR_IO, false, "Failed to construct FPA for %s", filename);
+        return NULL;
+    }
+
+    // load the given filerule (from config->camera) and bind it to the fpa
+    // the returned file is just a view to the entry on config->files
+    pmFPAfile *file = pmFPAfileDefineInput (config, fpa, filename);
+    psFree (fpa);
+    if (!file) {
+        psError(PS_ERR_IO, false, "file %s not defined\n", filename);
+        return NULL;
+    }
+
+    // image names may not come from file->names
+    if (!strcasecmp (file->filerule, "@FILES")) {
+        psError(PS_ERR_IO, true, "supplied filerule uses illegal value @FILES");
+        // XXX remove the file from config->files
+        psFree(file);
+        return NULL;
+    }
+
+    // image names may come from the detrend database
+    if (!strcasecmp (file->filerule, "@DETDB")) {
+        psTrace ("pmFPAfile", 5, "requiring use of detrend database source\n");
+        // don't free the file here: it is left on config->files
+        // to be used optionally by pmFPAfileDefineFromDetDB (or others)
+        if (success) *success = true;
+        return NULL;
+    }
+
+    // Prepend the global path to the file rule
+    // this function is implicitly an INPUT operation: do not create the file
+    psString tmpName = pmConfigConvertFilename (file->filerule, config, false);
+    psFree (file->filerule);
+    file->filerule = tmpName;
+
+    if (success) *success = true;
+
+    return file;
+}
+
+// construct an FPA based on the supplied config->camera
+// built the association between the FPA elements (CHIP/CELL) and the files
+// define the pmFPAfile filename and bind it to this FPA
+// save the pmFPAfile on config->files
+// return the pmFPAfile (a view to the one saved on config->files)
+pmFPAfile *pmFPAfileDefineFromDetDB (bool *success, const pmConfig *config, const char *filename,
+                                     pmFPA *input, pmDetrendType type)
+{
+    PS_ASSERT_PTR_NON_NULL(input, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(config->camera, NULL);
+    PS_ASSERT_PTR_NON_NULL(config->files, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+
+    bool status;
+    pmFPA *fpa = NULL;
+    pmFPAfile *file = NULL;
+
+    if (success) *success = false;
+
+    // a camera config is needed (as source of file rule)
+    if (config->camera == NULL) {
+        psError(PS_ERR_IO, true, "camera is not defined");
+        return NULL;
+    }
+    // a camera config is needed (as source of file rule)
+    if (config->cameraName == NULL) {
+        psAbort("camera defined but not cameraName!");
+    }
+
+    // find or define a pmFPAfile with this name
+    file = psMetadataLookupPtr (NULL, config->files, filename);
+    if (!file) {
+        // build the template fpa, set up the basic view
+        fpa = pmFPAConstruct (config->camera);
+        if (!fpa) {
+            psError(PS_ERR_IO, false, "Failed to construct FPA for %s", filename);
+            return NULL;
+        }
+        // load the given filerule (from config->camera) and bind it to the fpa
+        // the returned file is just a view to the entry on config->files
+        file = pmFPAfileDefineInput (config, fpa, filename);
+        if (!file) {
+            psError(PS_ERR_IO, false, "file %s not defined\n", filename);
+            psFree (fpa);
+            return NULL;
+        }
+    }
+
+    // we are constructing a detselect command of the form:
+    //   detselect -search -inst (camera) -type (type) -time (time) [others]
+    // camera, type, and time are derived from pmFPA *input, other options are
+    // added if specified for the particular detrend type by the DETREND.CONSTRAINTS
+    // note that the filter-dependent choices are set for ppImage in ppImageParseCamera
+    // XXX make all of the detrend constraints explicit in DETREND.CONSTRAINTS?
+
+    // Get the time from FPA.TIME
+    psTime *time = psMetadataLookupPtr(NULL, input->concepts, "FPA.TIME");
+    if (time->sec == 0 && time->nsec == 0) {
+        psLogMsg ("psModules.camera", PS_LOG_WARN, "FPA.TIME has not been set.\n");
+    }
+    pmDetrendSelectOptions *options = pmDetrendSelectOptionsAlloc(config->cameraName, *time, type);
+
+    // add additional constraints based on the type defined in the PPIMAGE recipe
+    // XXX use PPIMAGE or DETREND for the recipe name?
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, "PPIMAGE");
+    if (!status) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "PPIMAGE recipe not found.");
+        psFree(options);
+        psFree(fpa);
+        return false;
+    }
+    psMetadata *detConstraints = psMetadataLookupPtr (&status, recipe, "DETREND.CONSTRAINTS");
+    if (!status) {
+        psWarning("DETREND.CONSTRAINTS not found --- no constraints will be applied.");
+        goto DETREND_SELECT;
+    }
+
+    psString typeName = pmDetrendTypeToString (type);
+    psMetadata *constraints = psMetadataLookupPtr (&status, detConstraints, typeName);
+    if (!status) {
+        psWarning("DETREND.CONSTRAINTS for type %s not found --- no contraints will be applied.", typeName);
+        psFree(typeName);
+        goto DETREND_SELECT;
+    }
+    psFree(typeName);
+
+    // loop over the constraints and include in the detselect options
+    psMetadataIterator *iter = psMetadataIteratorAlloc (constraints, PS_LIST_HEAD, NULL);
+    psMetadataItem *item = NULL;
+    while ((item = psMetadataGetAndIncrement (iter)) != NULL) {
+        if (item->type != PS_DATA_STRING) {
+            psWarning("Invalid type for DETREND.CONSTRAINT element %s --- ignoring constraint", item->name);
+            continue;
+        }
+        char *option  = item->name;     // item->name must correspond to a valid detselect option
+        char *concept = item->data.V;
+
+        // these items refer to the corresponding values for the input image
+        // (ie, -filter input:filter or -exptime input:exptime)
+        if (!strcasecmp (option, "filter")) {
+            options->filter = psMetadataLookupPtr (&status, input->concepts, concept);
+            psMemIncrRefCounter (options->filter);
+            if (!status)
+                psAbort("failed to find filter (concept %s)", concept);
+        } else if (!strcasecmp (option, "exptime")) {
+            options->exptime = psMetadataLookupF32 (&status, input->concepts, concept);
+            options->exptimeSet = true;
+            if (!status)
+                psAbort("exptime not found (concept %s)", concept);
+        } else if (!strcasecmp (option, "airmass")) {
+            options->airmass = psMetadataLookupF32 (&status, input->concepts, concept);
+            options->airmassSet = true;
+            if (!status)
+                psAbort("airmass not found (concept %s)", concept);
+        } else if (!strcasecmp (option, "dettemp")) {
+            options->dettemp = psMetadataLookupF32 (&status, input->concepts, concept);
+            options->dettempSet = true;
+            if (!status)
+                psAbort("dettemp not found (concept %s)", concept);
+        } else if (!strcasecmp (option, "twilight")) {
+            options->twilight = psMetadataLookupF32 (&status, input->concepts, concept);
+            options->twilightSet = true;
+            if (!status)
+                psAbort("twilight not found (concept %s)", concept);
+        }
+
+        // the version is applied literally
+        if (!strcasecmp (option, "version")) {
+            options->version = psMemIncrRefCounter (concept);
+        }
+        // we can override the detrend database dettype if desired
+        // ie, use DOMEFLAT for type FLAT
+        // the dettype string is applied literally
+        if (!strcasecmp (option, "dettype")) {
+            options->dettype = psMemIncrRefCounter (concept);
+        }
+    }
+    psFree(iter);
+
+DETREND_SELECT:
+    {
+        // search for existing detrend data (detID)
+        pmDetrendSelectResults *results = pmDetrendSelect (options, config);
+        if (!results) {
+            psError (PS_ERR_IO, false, "no matching detrend data");
+            return NULL;
+        }
+        file->detrend = results;
+    }
+
+    psFree (options);
+
+    if (success) *success = true;
+    return file;
+}
+
+// create a new output pmFPAfile based on an existing FPA
+// only valid for pmFPAfile->mode == WRITE (or internal?)
+pmFPAfile *pmFPAfileDefineFromFPA (const pmConfig *config, pmFPA *src, int xBin, int yBin, const char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(src, false);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+
+    pmFPA *fpa = pmFPAConstruct(src->camera);
+    // XXX should this use DefineOutputForFormat?
+    pmFPAfile *file = pmFPAfileDefineOutput (config, fpa, filename);
+    if (!file) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "file %s not defined\n", filename);
+        return NULL;
+    }
+    file->src = psMemIncrRefCounter(src); // inherit output elements from this source pmFPA
+    file->xBin = xBin;
+    file->yBin = yBin;
+    psFree (fpa);
+    return file;
+}
+
+pmFPAfile *pmFPAfileDefineFromFile(const pmConfig *config, pmFPAfile *src, int xBin, int yBin,
+                                   const char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(src, false);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+
+    pmFPAfile *file = pmFPAfileDefineOutputForFormat(config, NULL, filename, src->cameraName,
+                                                     src->formatName);
+    file->src = psMemIncrRefCounter(src->fpa); // inherit output elements from this source pmFPA
+    file->xBin = xBin;
+    file->yBin = yBin;
+    return file;
+}
+
+// create a new output pmFPAfile based on an existing FPA
+// only valid for pmFPAfile->mode == WRITE (or internal?)
+pmFPAfile *pmFPAfileDefineNewCamera (const pmConfig *config, const char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+
+    pmFPAfile *file = pmFPAfileDefineOutput (config, NULL, filename);
+    if (!file) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "file %s not defined\n", filename);
+        return NULL;
+    }
+    if (!file->camera) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "file %s does not define a new camera\n", filename);
+        return NULL;
+    }
+    file->fpa = pmFPAConstruct(file->camera);
+
+    return file;
+}
+
+pmFPAfile *pmFPAfileDefineSkycell(const pmConfig *config, pmFPA *fpa, const char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(config->cameraName, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(config->formatName, NULL);
+
+    pmFPAfile *file;                    // The new file
+
+    if (config->cameraName[0] == '_' &&
+        strcmp(config->cameraName + strlen(config->cameraName) - 8, "-SKYCELL") == 0) {
+        // The input camera is already a skycell
+        file = pmFPAfileDefineOutputForFormat(config, fpa, filename, config->cameraName, "SKYCELL");
+    } else {
+        psString cameraName = NULL;         // Name of the old camera configuration
+        if (config->cameraName[0] == '_' &&
+            strcmp(config->cameraName + strlen(config->cameraName) - 5, "-CHIP") == 0) {
+            cameraName = psStringNCopy(config->cameraName + 1, strlen(config->cameraName) - 6);
+        } else if (config->cameraName[0] == '_' &&
+                   strcmp(config->cameraName + strlen(config->cameraName) - 4 , "-FPA") == 0) {
+            cameraName = psStringNCopy(config->cameraName + 1, strlen(config->cameraName) - 5);
+        } else {
+            cameraName = psMemIncrRefCounter(config->cameraName);
+        }
+        psString newCameraName = NULL;  // Name of the new (automatically-generated) camera configuration
+        psStringAppend(&newCameraName, "_%s-SKYCELL", cameraName);
+        file = pmFPAfileDefineOutputForFormat(config, fpa, filename, newCameraName, "SKYCELL");
+        psFree(cameraName);
+        psFree(newCameraName);
+    }
+    if (!file) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "file %s not defined\n", filename);
+        return NULL;
+    }
+
+    // Ensure everything is written out at the appropriate level
+    file->fileLevel = PM_FPA_LEVEL_FPA;
+    file->dataLevel = PM_FPA_LEVEL_FPA;
+    file->freeLevel = PM_FPA_LEVEL_FPA;
+
+    return file;
+}
+
+pmFPAfile *pmFPAfileDefineChipMosaic(const pmConfig *config, pmFPA *src, const char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(src, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(config->cameraName, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(config->formatName, NULL);
+
+    pmFPAfile *file;                    // The new file
+    if (config->cameraName[0] == '_' &&
+        (strcmp(config->cameraName + strlen(config->cameraName) - 5, "-CHIP") == 0 ||
+         strcmp(config->cameraName + strlen(config->cameraName) - 8, "-SKYCELL") == 0)) {
+        // The input camera has already been mosaicked to this level
+        file = pmFPAfileDefineOutputForFormat(config, NULL, filename, config->cameraName, config->formatName);
+    } else {
+        psString cameraName = NULL; // Name of the new (automatically-generated) camera configuration
+        if (config->cameraName[0] == '_' &&
+            strcmp(config->cameraName + strlen(config->cameraName) - 4 , "-FPA") == 0) {
+            cameraName = psStringNCopy(config->cameraName + 1, strlen(config->cameraName) - 5);
+        } else {
+            cameraName = psMemIncrRefCounter(config->cameraName);
+        }
+        psString newCameraName = NULL;  // Name of the new (automatically-generated) camera configuration
+        psStringAppend(&newCameraName, "_%s-CHIP", cameraName);
+
+        // Find the correct camera configuration
+        file = pmFPAfileDefineOutputForFormat(config, NULL, filename, newCameraName, config->formatName);
+        psFree(newCameraName);
+        psFree(cameraName);
+    }
+    if (!file) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "file %s not defined\n", filename);
+        return NULL;
+    }
+
+    file->src = psMemIncrRefCounter(src); // inherit output elements from this source pmFPA
+
+    file->mosaicLevel = PM_FPA_LEVEL_CHIP; // don't do any I/O on this at a lower level
+
+    return file;
+}
+
+pmFPAfile *pmFPAfileDefineFPAMosaic(const pmConfig *config, pmFPA *src, const char *filename)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(src, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(config->cameraName, NULL);
+
+    pmFPAfile *file;                    // The new file
+    if (config->cameraName[0] == '_' &&
+        (strcmp(config->cameraName + strlen(config->cameraName) - 4 , "-FPA") == 0 ||
+         strcmp(config->cameraName + strlen(config->cameraName) - 8, "-SKYCELL") == 0)) {
+        // The input camera has already been mosaicked to this level
+        file = pmFPAfileDefineOutputForFormat(config, NULL, filename, config->cameraName, config->formatName);
+    } else {
+
+        psString original = NULL;       // Name of the original camera configuration
+        if (config->cameraName[0] == '_' &&
+            strcmp(config->cameraName + strlen(config->cameraName) - 5 , "-CHIP") == 0) {
+            // It's a chip mosaic; we need to get the original name
+            original = psStringNCopy(config->cameraName + 1, strlen(config->cameraName) - 6);
+        } else if (config->cameraName[0] == '_' &&
+            strcmp(config->cameraName + strlen(config->cameraName) - 8, "-SKYCELL") == 0) {
+            original = psStringNCopy(config->cameraName + 1, strlen(config->cameraName) - 9);
+        } else {
+            original = psMemIncrRefCounter(config->cameraName);
+        }
+        psString cameraName = NULL;
+        psStringAppend(&cameraName, "_%s-FPA", original);
+        psFree(original);
+
+        file = pmFPAfileDefineOutputForFormat(config, NULL, filename, cameraName, config->formatName);
+        psFree(cameraName);
+    }
+    if (!file) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "file %s not defined\n", filename);
+        return NULL;
+    }
+
+    file->src = psMemIncrRefCounter(src); // inherit output elements from this source pmFPA
+
+    file->mosaicLevel = PM_FPA_LEVEL_FPA; // don't do any I/O on this at a lower level
+
+    return file;
+}
+
+// create a file with the given name, assign it type "INTERNAL", and supply it with an image
+// of the requested dimensions. (image only, mask and weight are ignored)
+pmReadout *pmFPAfileDefineInternal (psMetadata *files, const char *name, int Nx, int Ny, int type)
+{
+    PS_ASSERT_PTR_NON_NULL(files, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    pmReadout *readout = pmReadoutAlloc(NULL);
+    readout->image = psImageAlloc(Nx, Ny, type);
+
+    // I want an image from the
+    pmFPAfile *file = pmFPAfileAlloc();
+    file->mode = PM_FPA_MODE_INTERNAL;
+    file->name = psStringCopy (name);
+
+    file->readout = readout;
+    psMetadataAddPtr(files, PS_LIST_TAIL, name, PS_DATA_UNKNOWN, "", file);
+    psFree(file);
+    // we free this copy of file, but 'files' still has a copy
+
+    return readout;
+}
+
+bool pmFPAfileDropInternal(psMetadata *files, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(files, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    bool status = false;
+
+    pmFPAfile *file = psMetadataLookupPtr (&status, files, name);
+    if (!status) {
+        psTrace("psModules.camera", 6, "Internal File %s not in file list", name);
+        return true;
+    }
+    if (file == NULL) {
+        psError(PS_ERR_IO, true, "file %s is NULL", name);
+        return false;
+    }
+    if (file->mode != PM_FPA_MODE_INTERNAL) {
+        psTrace("psModules.camera", 6, "FPA File %s not Internal, not dropping", name);
+        return true;
+    }
+
+    psTrace("psModules.camera", 6, "dropping Internal FPA File %s", name);
+    psMetadataRemoveKey (files, name);
+    return true;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileDefine.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileDefine.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileDefine.h	(revision 15358)
@@ -0,0 +1,136 @@
+/* @file  pmFPAview.h
+ * @brief Tools to manipulate the FPA structure elements.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-09-19 21:37:58 $
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+#ifndef PM_FPA_FILE_DEFINE_H
+#define PM_FPA_FILE_DEFINE_H
+
+// load the pmFPAfile information from the camera configuration data
+//
+// Note that the returned pmFPAfile is a view only, so it should not be freed by the caller --- the only
+// reference count is held by the config->files metadata.  Multiple file rules of the same name are permitted
+// if multiple is true.
+pmFPAfile *pmFPAfileDefineInput (const pmConfig *config, pmFPA *fpa, const char *name);
+
+// load the pmFPAfile information from the camera configuration data
+//
+// Note that the returned pmFPAfile is a view only, so it should not be freed by the caller --- the only
+// reference count is held by the config->files metadata.
+// Define an output pmFPAfile
+pmFPAfile *pmFPAfileDefineOutput(const pmConfig *config, // Configuration
+                                 pmFPA *fpa, // Optional FPA to bind
+                                 const char *name // Name of file rule
+    );
+
+/// Same as pmFPAfileDefineOutput, but binds to the fpa in the provided file
+pmFPAfile *pmFPAfileDefineOutputFromFile(const pmConfig *config, // Configuration
+                                         pmFPAfile *file, // File to bind FPAs, or NULL
+                                         const char *name // Name of file rule
+    );
+
+/// Define the FPA file using the provided camera and format names.
+pmFPAfile *pmFPAfileDefineOutputForFormat(const pmConfig *config, // Configuration
+                                          pmFPA *fpa, // Optional FPA to bind
+                                          const char *name, // Name of file rule
+                                          psString cameraName, // Name of camera configuration to use
+                                          psString formatName // Name of camera format to use
+    );
+
+// look for the given argname on the argument list.  find the give filename from the file rules
+//
+// Note that the returned pmFPAfile is a view only, so it should not be freed by the caller --- the only
+// reference count is held by the config->files metadata.
+pmFPAfile *pmFPAfileDefineFromArgs (bool *found, pmConfig *config, const char *filename, const char *argname);
+
+// look for the given argname on the argument list; bind the associated files to the specified
+// fpa.  these are, eg, mask or weight images.
+// Note that the returned pmFPAfile is a view only, so it should not be freed by the caller --- the only
+// reference count is held by the config->files metadata.
+pmFPAfile *pmFPAfileBindFromArgs (bool *found, pmFPAfile *input, pmConfig *config, const char *filename, const char *argname);
+
+// look for the given argname on the argument list.  find the give filename from the file rules
+//
+// Note that the returned pmFPAfile is a view only, so it should not be freed by the caller --- the only
+// reference count is held by the config->files metadata.
+pmFPAfile *pmFPAfileDefineFromConf (bool *found, const pmConfig *config, const char *filename);
+
+// look for the given argname on the argument list.  find the give filename from the file rules
+//
+// Note that the returned pmFPAfile is a view only, so it should not be freed by the caller --- the only
+// reference count is held by the config->files metadata.
+pmFPAfile *pmFPAfileDefineFromDetDB (bool *found, const pmConfig *config, const char *filename,
+                                     pmFPA *input, pmDetrendType type);
+
+// create a new output pmFPAfile based on an existing FPA
+//
+// Note that the returned pmFPAfile is a view only, so it should not be freed by the caller --- the only
+// reference count is held by the config->files metadata.
+pmFPAfile *pmFPAfileDefineFromFPA (const pmConfig *config, pmFPA *src, int xBin, int yBin, const char *filename);
+
+/// Same as pmFPAfileDefineFromFPA, except it uses an FPA file instead of an FPA
+pmFPAfile *pmFPAfileDefineFromFile(const pmConfig *config, // Configuration
+                                   pmFPAfile *src, // Source file for this file
+                                   int xBin, int yBin, // Binning for this file
+                                   const char *filename // Name of file rule
+    );
+
+
+// create a new output pmFPAfile based on an existing FPA
+// only valid for pmFPAfile->mode == WRITE (or internal?)
+//
+// Note that the returned pmFPAfile is a view only, so it should not be freed by the caller --- the only
+// reference count is held by the config->files metadata.
+pmFPAfile *pmFPAfileDefineNewCamera (const pmConfig *config, const char *filename);
+
+/// Create a new output pmFPAfile for a skycell of the default camera
+///
+/// The new pmFPAfile is inserted into the config->files metadata, freed and returned; so that the user does
+/// not have to (and should not!) free the result.
+pmFPAfile *pmFPAfileDefineSkycell(const pmConfig *config, ///< Configuration data
+                                  pmFPA *fpa, ///< FPA to which to bind
+                                  const char *filename ///< Output (root) filename
+    );
+
+
+/// Create a new output pmFPAfile based upon a chip mosaic of an existing FPA
+///
+/// The new pmFPAfile is inserted into the config->files metadata, freed and returned; so that the user does
+/// not have to (and should not!) free the result.
+pmFPAfile *pmFPAfileDefineChipMosaic(const pmConfig *config, ///< Configuration data
+                                     pmFPA *src, ///< Source FPA
+                                     const char *filename ///< Output (root) filename
+                                    );
+
+/// Create a new output pmFPAfile based upon an FPA mosaic of an existing FPA
+///
+/// The new pmFPAfile is inserted into the config->files metadata, freed and returned; so that the user does
+/// not have to (and should not!) free the result.
+pmFPAfile *pmFPAfileDefineFPAMosaic(const pmConfig *config, ///< Configuration data
+                                    pmFPA *src, ///< Source FPA
+                                    const char *filename ///< Output (root) filename
+                                   );
+
+// create a file with the given name, assign it type "INTERNAL", and supply it with an image
+// of the requested dimensions. (image only, mask and weight are ignored)
+pmReadout *pmFPAfileDefineInternal (psMetadata *files, const char *name, int Nx, int Ny, int type);
+
+// delete the INTERNAL file of the given name (if it exists)
+bool pmFPAfileDropInternal (psMetadata *files, const char *name);
+
+// look for the given argname on the argument list.  find the give filename from the file rules
+//
+// Note that the returned pmFPAfile is a view only, so it should not be freed by the caller --- the only
+// reference count is held by the config->files metadata.
+pmFPAfile *pmFPAfileDefineSingleFromArgs (bool *found, pmConfig *config, const char *filename,
+        const char *argname, int entry);
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileFitsIO.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileFitsIO.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileFitsIO.c	(revision 15358)
@@ -0,0 +1,509 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPARead.h"
+#include "pmFPAWrite.h"
+#include "pmFPAMaskWeight.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAfileFitsIO.h"
+#include "pmFPACopy.h"
+#include "pmFPAConstruct.h"
+
+pmFPA *pmFPAfileSuitableFPA(const pmFPAfile *file, const pmFPAview *view, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(file, NULL);
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+
+    if (!file->format) {                // Working with the same output format as input format
+        return psMemIncrRefCounter(file->fpa);
+    }
+
+    // May need to change format
+    pmFPALevel level = pmFPAviewLevel(view); // Level for the view
+    if (level == PM_FPA_LEVEL_NONE || level == PM_FPA_LEVEL_READOUT) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "This function shouldn't be called at the readout (or unknown) level.");
+        return NULL;
+    }
+
+    // Does the HDU of interest conform to the desired format?
+    pmHDU *hdu = pmFPAviewThisHDU(view, file->fpa); // The HDU of interest
+    if (hdu && hdu->format == file->format) {
+        // No work required
+        return psMemIncrRefCounter(file->fpa);
+    }
+
+    // Otherwise, we have to generate a copy with the correct format
+
+    pmFPAview *phuView = pmFPAviewAlloc(0); // View corresponding to the PHU
+    *phuView = *view;               // Copy contents
+    pmFPALevel phuLevel = pmFPAPHULevel(file->format); // Level for the PHU
+    switch (phuLevel) {
+      case PM_FPA_LEVEL_FPA:
+        phuView->chip = -1;
+        // Flow through
+      case PM_FPA_LEVEL_CHIP:
+        phuView->cell = -1;
+        // Flow through
+      case PM_FPA_LEVEL_CELL:
+        phuView->readout = -1;
+        break;
+      case PM_FPA_LEVEL_READOUT:
+      case PM_FPA_LEVEL_NONE:
+      default:
+        psAbort("Should never get here: bad phu level.\n");
+    }
+
+    pmFPA *nameSource = file->src; // Source of FPA.NAME
+    if (!nameSource) {
+        nameSource = file->fpa;
+    }
+    bool mdok;                  // Status of MD lookup
+    const char *fpaname = psMetadataLookupStr(&mdok, nameSource->concepts, "FPA.NAME"); // Name of FPA
+
+    pmFPA *copy = pmFPAConstruct(file->camera);  // FPA to return
+    if (!pmFPAAddSourceFromView(copy, fpaname, phuView, file->format)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to insert HDU into FPA for writing.\n");
+        psFree(copy);
+        psFree(phuView);
+        return NULL;
+    }
+    psFree(phuView);
+
+    switch (level) {
+      case PM_FPA_LEVEL_FPA:
+        if (!pmFPACopy(copy, file->fpa)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to copy FPA for format conversion.\n");
+            return NULL;
+        }
+        return copy;
+      case PM_FPA_LEVEL_CHIP: {
+          pmChip *chip = pmFPAviewThisChip(view, copy); // Chip of interest
+          pmChip *srcChip = pmFPAviewThisChip(view, file->fpa); // Source chip
+          if (!pmChipCopy(chip, srcChip)) {
+              psError(PS_ERR_UNKNOWN, false, "Unable to copy chip for format conversion.\n");
+              return false;
+          }
+          return copy;
+      }
+      case PM_FPA_LEVEL_CELL: {
+          pmCell *cell = pmFPAviewThisCell(view, copy); // Cell of interest
+          pmCell *srcCell = pmFPAviewThisCell(view, file->fpa); // Source cell
+          if (!pmCellCopy(cell, srcCell)) {
+              psError(PS_ERR_UNKNOWN, false, "Unable to copy cell for format conversion.\n");
+              return false;
+          }
+          return copy;
+      }
+      case PM_FPA_LEVEL_READOUT:
+      case PM_FPA_LEVEL_NONE:
+      default:
+        psAbort("Should never get here: bad phu level.\n");
+    }
+
+    return NULL;
+
+}
+
+// given an already-opened fits file, read the table corresponding to the specified view
+bool pmFPAviewReadFitsTable(const pmFPAview *view, pmFPAfile *file, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    pmFPA *fpa = file->fpa;             // FPA of interest
+    psFits *fits = file->fits;          // FITS file
+
+    if (view->chip == -1) {
+        return pmFPAReadTable(fpa, fits, name) > 0;
+    }
+
+    if (view->cell == -1) {
+        pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
+        return pmChipReadTable(chip, fits, name) > 0;
+    }
+
+    pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
+    return pmCellReadTable(cell, fits, name) > 0;
+}
+
+// given an already-opened fits file, write the table corresponding to the specified view
+bool pmFPAviewWriteFitsTable(const pmFPAview *view, pmFPAfile *file, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    pmFPA *fpa = file->fpa;             // FPA of interest
+    psFits *fits = file->fits;          // FITS file
+
+    if (view->chip == -1) {
+        return pmFPAWriteTable(fits, fpa, name) > 0;
+    }
+
+    if (view->cell == -1) {
+        pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
+        return pmChipWriteTable(fits, chip, name) > 0;
+    }
+
+    pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
+    return pmCellWriteTable(fits, cell, name) > 0;
+}
+
+
+// given an already-opened fits file, read the components corresponding to the specified view
+static bool fpaViewReadFitsImage(const pmFPAview *view, // FPA view, specifying the level of interest
+                                 pmFPAfile *file, // FPA file of interest
+                                 bool (*fpaReadFunc)(pmFPA*, psFits*, psDB*), // Function to read FPA
+                                 bool (*chipReadFunc)(pmChip*, psFits*, psDB*), // Function to read chip
+                                 bool (*cellReadFunc)(pmCell*, psFits*, psDB*) // Function to read cell
+                                )
+{
+    assert(view);
+    assert(file);
+
+    pmFPA *fpa = file->fpa;             // FPA of interest
+    psFits *fits = file->fits;          // FITS file from which to read
+
+    if (view->chip == -1) {
+        return fpaReadFunc(fpa, fits, NULL);
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip]; // Chip of interest
+
+    if (view->cell == -1) {
+        return chipReadFunc(chip, fits, NULL);
+    }
+
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell]; // Cell of interest
+
+    if (view->readout == -1) {
+        return cellReadFunc(cell, fits, NULL);
+    }
+    psError(PS_ERR_UNKNOWN, true, "Bad view: %d,%d", view->chip, view->cell);
+    return false;
+
+    // XXX pmReadoutRead, pmReadoutReadSegement disabled for now
+    #if 0
+
+    if (view->readout >= cell->readouts->n) {
+        psError(PS_ERR_IO, true, "Requested readout == %d >= cell->readouts->n == %d",
+                view->readout, cell->readouts->n);
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    if (view->nRows == 0) {
+        pmReadoutRead (readout, fits, NULL);
+    } else {
+        pmReadoutReadSegment (readout, fits, view->nRows, view->iRows, NULL, NULL);
+    }
+    return true;
+    #endif
+}
+
+
+bool pmFPAviewReadFitsImage(const pmFPAview *view, pmFPAfile *file)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewReadFitsImage(view, file, pmFPARead, pmChipRead, pmCellRead);
+}
+
+bool pmFPAviewReadFitsMask(const pmFPAview *view, pmFPAfile *file)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewReadFitsImage(view, file, pmFPAReadMask, pmChipReadMask, pmCellReadMask);
+}
+
+bool pmFPAviewReadFitsWeight(const pmFPAview *view, pmFPAfile *file)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewReadFitsImage(view, file, pmFPAReadWeight, pmChipReadWeight, pmCellReadWeight);
+}
+
+bool pmFPAviewReadFitsHeaderSet(const pmFPAview *view, pmFPAfile *file)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewReadFitsImage(view, file, pmFPAReadHeaderSet, pmChipReadHeaderSet, pmCellReadHeaderSet);
+}
+
+// given an already-opened fits file, write the components corresponding
+// to the specified view. when the file was opened, pmFPA/Chip/CellWrite was
+// called on it with blank=true to write the (possible) blank PHU
+// do NOT call the functions below with blank=true or they will write
+// out data in an inconsistent fashion
+// the calls below should recurse down the element to write out all components.
+static bool fpaViewWriteFitsImage(const pmFPAview *view, // FPA view, specifying the level of interest
+                                  pmFPAfile *file, // FPA file of interest
+                                  const pmConfig *config, // Configuration
+                                  bool (*fpaWriteFunc)(pmFPA*, psFits*, psDB*, bool, bool), // Func for FPA
+                                  bool (*chipWriteFunc)(pmChip*, psFits*, psDB*, bool, bool), // Func for chip
+                                  bool (*cellWriteFunc)(pmCell*, psFits*, psDB*, bool) // Func for cell
+                                 )
+{
+    assert(view);
+    assert(file);
+
+    psFits *fits = file->fits;          // FITS file
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config); // FPA to write
+
+    switch (pmFPAviewLevel(view)) {
+    case PM_FPA_LEVEL_FPA: {
+            bool success = fpaWriteFunc(fpa, fits, NULL, false, true);
+            psFree(fpa);
+            return success;
+        }
+    case PM_FPA_LEVEL_CHIP: {
+            pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
+            bool success = chipWriteFunc(chip, fits, NULL, false, true);
+            psFree(fpa);
+            return success;
+        }
+    case PM_FPA_LEVEL_CELL: {
+            pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
+            bool success = cellWriteFunc(cell, fits, NULL, false);
+            psFree(fpa);
+            return success;
+        }
+    case PM_FPA_LEVEL_READOUT:
+        #if 0 // XXX disable readout write for now
+
+        {
+            pmReadout *readout = pmFPAviewThisReadout(view, file->fpa); // Readout of interest
+            if (changeFormat)
+        {
+            // No copy function defined for readouts!
+            psError(PS_ERR_UNKNOWN, false, "Unable to copy readout for format conversion on write.\n");
+                return false;
+            }
+            if (view->nRows == 0)
+        {
+            return pmReadoutWrite(readout, fits, NULL, NULL);
+            } else
+            {
+                return pmReadoutWriteSegment(readout, fits, view->nRows, view->iRows, NULL, NULL);
+            }
+        }
+        #endif
+    case PM_FPA_LEVEL_NONE:
+    default:
+        psAbort("Should never reach here: invalid file level.");
+    }
+
+    return false;
+}
+
+bool pmFPAviewWriteFitsImage(const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewWriteFitsImage(view, file, config, pmFPAWrite, pmChipWrite, pmCellWrite);
+}
+
+bool pmFPAviewWriteFitsMask(const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewWriteFitsImage(view, file, config, pmFPAWriteMask, pmChipWriteMask, pmCellWriteMask);
+}
+
+bool pmFPAviewWriteFitsWeight(const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    return fpaViewWriteFitsImage(view, file, config, pmFPAWriteWeight, pmChipWriteWeight, pmCellWriteWeight);
+}
+
+// given an already-opened fits file, read the components corresponding
+// to the specified view
+bool pmFPAviewFreeData(const pmFPAview *view, pmFPAfile *file)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        psTrace ("pmFPAfile", 5, "freeing fpa for %s\n", file->filename);
+        pmFPAFreeData (fpa);
+        // XXX drop me: file->fpa = NULL;
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        psTrace ("pmFPAfile", 5, "freeing chip %d for %s\n", view->chip, file->filename);
+        pmChipFreeData (chip);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        psTrace ("pmFPAfile", 5, "freeing cell %d for %s\n", view->cell, file->filename);
+        pmCellFreeData (cell);
+        return true;
+    }
+    psError(PS_ERR_UNKNOWN, true, "Returning false");
+    return false;
+
+    // XXX pmReadoutRead, pmReadoutReadSegement disabled for now
+    #if 0
+
+    if (view->readout >= cell->readouts->n) {
+        psError(PS_ERR_IO, true, "Requested readout == %d >= cell->readouts->n == %d",
+                view->readout, cell->readouts->n);
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    if (view->nRows == 0) {
+        pmReadoutRead (readout, fits, NULL);
+    } else {
+        pmReadoutReadSegment (readout, fits, view->nRows, view->iRows, NULL, NULL);
+    }
+    return true;
+    #endif
+}
+
+#if 0
+// Shouldn't need this --- when we want to free fringe data, we want to free the whole level, not just the
+// table.
+
+// Free the table within a cell
+static void freeTable(pmCell *cell,     // Cell of interest
+                      const char *name  // Name of table to free
+                     )
+{
+    assert(cell);
+    assert(name && strlen(name) > 0);
+
+    psString headerName = NULL;         // Name of header
+    psStringAppend(&headerName, "%s.HEADER", name);
+    if (psMetadataLookup(cell->analysis, headerName)) {
+        psMetadataRemoveKey(cell->analysis, headerName);
+    }
+    psFree(headerName);
+
+    if (psMetadataLookup(cell->analysis, name)) {
+        psMetadataRemoveKey(cell->analysis, name);
+    }
+
+    return;
+}
+
+// given a file, free the components corresponding to the specified view
+bool pmFPAviewFreeFitsTable (const pmFPAview *view, pmFPAfile *file, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        psArray *chips = fpa->chips;    // Array of chips
+        for (int i = 0; i < chips->n; i++) {
+            pmChip *chip = chips->data[i]; // Chip of interest
+            psArray *cells = chip->cells; // Array of cells
+            for (int j = 0; j < cells->n; j++) {
+                pmCell *cell = cells->data[j]; // Cell of interest
+                freeTable(cell, name);
+            }
+        }
+        return true;
+    }
+
+    if (view->cell == -1) {
+        pmChip *chip = pmFPAviewThisChip(view, fpa); // Chip of interest
+        psArray *cells = chip->cells;   // Array of cells
+        for (int i = 0; i < cells->n; i++) {
+            pmCell *cell = cells->data[i]; // Cell of interest
+            freeTable(cell, name);
+        }
+        return true;
+    }
+
+    pmCell *cell = pmFPAviewThisCell(view, fpa); // Cell of interest
+    freeTable(cell, name);
+    return true;
+}
+
+#endif
+
+bool pmFPAviewFitsWritePHU (const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    bool status = false;
+
+    if (file->mode != PM_FPA_MODE_WRITE) return true;
+    if (file->wrote_phu) return true;
+
+    // select or generate the desired fpa in the correct output format
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config);
+    pmHDU *phu = pmFPAviewThisHDU(view, fpa);
+    if (!phu || !phu->blankPHU) {
+        // No PHU to write!
+        psFree(fpa);
+        return true;
+    }
+
+    switch (file->fileLevel) {
+      case PM_FPA_LEVEL_FPA:
+        status = pmFPAWrite(fpa, file->fits, NULL, true, false);
+        break;
+      case PM_FPA_LEVEL_CHIP: {
+          pmChip *chip = pmFPAviewThisChip(view, fpa);
+          status = pmChipWrite(chip, file->fits, NULL, true, false);
+          break;
+      }
+      case PM_FPA_LEVEL_CELL: {
+          pmCell *cell = pmFPAviewThisCell(view, fpa);
+          status = pmCellWrite(cell, file->fits, NULL, true);
+          break;
+      }
+      default:
+        psAbort("fileLevel not correctly set");
+        break;
+    }
+
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "failed to write PHU for Image %s (%s)\n", file->filename, file->name);
+        return false;
+    }
+
+    psFree(fpa);
+    file->wrote_phu = true;
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileFitsIO.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileFitsIO.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileFitsIO.h	(revision 15358)
@@ -0,0 +1,88 @@
+/* @file  pmFPAview.h
+ * @brief Tools to manipulate the FPA structure elements.
+ *
+ * @author EAM, IfA
+ * @author PAP, IfA
+ *
+ * @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-07-14 03:19:01 $
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_FILE_FITS_IO_H
+#define PM_FPA_FILE_FITS_IO_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Read an image into the current view
+bool pmFPAviewReadFitsImage(const pmFPAview *view, ///< View specifying level of interest
+                            pmFPAfile *file ///< FPA file into which to read
+                           );
+
+/// Read a mask into the current view
+bool pmFPAviewReadFitsMask(const pmFPAview *view, ///< View specifying level of interest
+                           pmFPAfile *file ///< FPA file into which to read
+                          );
+/// Read a weight map into the current view
+bool pmFPAviewReadFitsWeight(const pmFPAview *view,  ///< View specifying level of interest
+                             pmFPAfile *file ///< FPA file into which to read
+                            );
+
+/// Read an image header into the current view
+bool pmFPAviewReadFitsHeaderSet(const pmFPAview *view,  ///< View specifying level of interest
+                                pmFPAfile *file ///< FPA file into which to read
+    );
+
+/// Write the image for the specified view
+bool pmFPAviewWriteFitsImage(const pmFPAview *view, ///< View specifying level of interest
+                             pmFPAfile *file, ///< FPA file to write
+                             const pmConfig *config ///< Configuration
+                            );
+
+/// Write the mask for the specified view
+bool pmFPAviewWriteFitsMask(const pmFPAview *view, ///< View specifying level of interest
+                            pmFPAfile *file, ///< FPA file to write
+                            const pmConfig *config ///< Configuration
+                           );
+
+/// Write the weight map for the specified view
+bool pmFPAviewWriteFitsWeight(const pmFPAview *view, ///< View specifying level of interest
+                              pmFPAfile *file, ///< FPA file to write
+                              const pmConfig *config ///< Configuration
+                             );
+
+/// Write a PHU for a fits image if needed
+bool pmFPAviewFitsWritePHU (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+
+/// Free the data for the specified view
+bool pmFPAviewFreeData(const pmFPAview *view, ///< View specifying level of interest
+                       pmFPAfile *file  ///< FPA file to free data
+                      );
+
+/// Read a table into the current view
+bool pmFPAviewReadFitsTable(const pmFPAview *view, ///<  View specifying level of interest
+                            pmFPAfile *file, ///< FPA file into which to read
+                            const char *name ///< Name of table
+                           );
+
+/// Write the table for the specified view
+bool pmFPAviewWriteFitsTable(const pmFPAview *view, ///<  View specifying level of interest
+                             pmFPAfile *file, ///< FPA file to write
+                             const char *name ///< Name of table
+                            );
+
+/// Produce a suitable FPA for writing, on the basis of the input FPAfile
+///
+/// A new FPA with a changed format is generated if required (file->format is set and file->camera is equal to
+/// the default, indicating a change in the format without changing the camera --- changes to the camera are
+/// handled using other systems --- see pmFPAfileDefineChipMosaic, pmFPAfileDefineFPAMosaic).  Otherwise the
+/// file->fpa is returned (incremented).
+pmFPA *pmFPAfileSuitableFPA(const pmFPAfile *file,///< File containing the fpa
+                            const pmFPAview *view, ///< View at which to produce the fpa
+                            const pmConfig *config ///< Configuration
+                           );
+
+/// @}
+
+# endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileIO.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileIO.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileIO.c	(revision 15358)
@@ -0,0 +1,933 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>            /* for strn?casecmp */
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAMaskWeight.h"
+#include "pmFPAview.h"
+#include "pmFPAFlags.h"
+#include "pmFPAfile.h"
+#include "pmFPACopy.h"
+#include "pmFPARead.h"
+#include "pmFPAWrite.h"
+#include "pmFPAfileIO.h"
+#include "pmFPAfileFitsIO.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmSourceIO.h"
+#include "pmResiduals.h"
+#include "pmPSF_IO.h"
+#include "pmFPA_JPEG.h"
+#include "pmSourcePlots.h"
+#include "pmFPAConstruct.h"
+#include "pmConcepts.h"
+
+// attempt create, read, write, close, or free pmFPAfiles available in files files are
+// automatically opened before they are read.  In the case of MEF files, the PHU is
+// read when the file is opened and written before the first extension is written.
+bool pmFPAfileIOChecks (pmConfig *config, const pmFPAview *view, pmFPAfilePlace place)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(config->files, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    psMetadata *files = config->files;
+
+    // attempt to perform all create, read, write, close operations
+    psMetadataItem *item = NULL;
+    psMetadataIterator *iter = psMetadataIteratorAlloc (files, PS_LIST_HEAD, NULL);
+    while ((item = psMetadataGetAndIncrement (iter)) != NULL) {
+        pmFPAfile *file = item->data.V;
+
+        switch (place) {
+        case PM_FPA_BEFORE:
+            if (!pmFPAfileRead (file, view, config)) {
+                psError(PS_ERR_IO, false, "failed READ in FPA_BEFORE block for %s", file->name);
+                goto failure;
+            }
+            if (!pmFPAfileCreate(file, view, config)) {
+                psError(PS_ERR_IO, false, "failed CREATE in FPA_BEFORE block for %s", file->name);
+                goto failure;
+            }
+            break;
+        case PM_FPA_AFTER:
+            if (!pmFPAfileWrite (file, view, config)) {
+                psError(PS_ERR_IO, false, "failed WRITE in FPA_AFTER block for %s", file->name);
+                goto failure;
+            }
+            if (!pmFPAfileClose(file, view)) {
+                psError(PS_ERR_IO, false, "failed CLOSE in FPA_AFTER block for %s", file->name);
+                goto failure;
+            }
+            break;
+        default:
+            psAbort("You can't get here");
+        }
+    }
+
+    // attempt to free data that is no longer needed
+    psMetadataIteratorSet (iter, PS_LIST_HEAD);
+    while ((item = psMetadataGetAndIncrement (iter)) != NULL) {
+        pmFPAfile *file = item->data.V;
+
+        switch (place) {
+        case PM_FPA_BEFORE:
+            break;
+        case PM_FPA_AFTER:
+            if (!pmFPAfileFreeData(file, view)) {
+                if (!psMetadataRemoveKey(files, file->name)) {
+                    psError(PS_ERR_IO, false, "failed to remove %s in FPA_AFTER block", file->name);
+                    goto failure;
+                }
+            }
+            break;
+        default:
+            psAbort("You can't get here");
+        }
+    }
+    psFree (iter);
+    return true;
+
+failure:
+    psFree (iter);
+    return false;
+}
+
+// read the file, if necessary and possible
+bool pmFPAfileRead(pmFPAfile *file, const pmFPAview *view, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    // an internal file should not be sent here (should not be left on config->files)
+    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);
+
+    // skip the following states
+    if (file->state & PM_FPA_STATE_INACTIVE) {
+        psTrace("psModules.camera", 6, "skip read for %s, file is inactive", file->name);
+        return true;
+    }
+    if (file->mode != PM_FPA_MODE_READ) {
+        psTrace("psModules.camera", 6, "skip read for %s, mode is not READ", file->name);
+        return true;
+    }
+
+    // get the current level
+    pmFPALevel level = pmFPAviewLevel (view);
+
+    // do we need to read this file? defer until we read the correct level
+    if (level != file->dataLevel) {
+        psTrace("psModules.camera", 6, "skip reading of %s at this level %s: dataLevel is %s",
+                file->name, pmFPALevelToName(level), pmFPALevelToName(file->dataLevel));
+        return true;
+    }
+
+    // do we need to open this file?
+    if (level >= file->fileLevel) {
+        // we are allowed to open a file at a level which is not the fileLevel, but we need to
+        // supply a view at the fileLevel for the file lookup functions below
+        pmFPAview *fileView = pmFPAviewForLevel (file->fileLevel, view);
+        if (!pmFPAfileOpen (file, fileView, config)) {
+            psError(PS_ERR_IO, false, "failed to open file %s (%s)", file->filename, file->name);
+            psFree (fileView);
+            return false;
+        }
+        psFree (fileView);
+    }
+
+    // We need to read it --- double-check it's open!
+    if (file->state == PM_FPA_STATE_CLOSED) {
+        psError(PS_ERR_IO, false, "failed to open file %s when attempting to read", file->name);
+        return false;
+    }
+
+    // select a reading method
+    bool status = true;
+    switch (file->type) {
+      case PM_FPA_FILE_IMAGE:
+        status = pmFPAviewReadFitsImage(view, file);
+        break;
+      case PM_FPA_FILE_MASK:
+        status = pmFPAviewReadFitsMask(view, file);
+        break;
+      case PM_FPA_FILE_WEIGHT:
+        status = pmFPAviewReadFitsWeight(view, file);
+        break;
+      case PM_FPA_FILE_HEADER:
+        status = pmFPAviewReadFitsHeaderSet(view, file);
+        break;
+      case PM_FPA_FILE_FRINGE:
+        status = pmFPAviewReadFitsImage (view, file);
+        if (status) {
+            if (!pmFPAviewReadFitsTable(view, file, "FRINGE")) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to read fringe data from %s.\n", file->filename);
+                return false;
+            }
+        }
+        break;
+      case PM_FPA_FILE_SX:
+      case PM_FPA_FILE_RAW:
+      case PM_FPA_FILE_OBJ:
+      case PM_FPA_FILE_CMP:
+      case PM_FPA_FILE_CMF:
+        status = pmFPAviewReadObjects (view, file, config);
+        break;
+      case PM_FPA_FILE_PSF:
+        status = pmPSFmodelReadForView (view, file, config);
+        break;
+      case PM_FPA_FILE_JPEG:
+      case PM_FPA_FILE_KAPA:
+        break;
+      default:
+        psError(PS_ERR_IO, true, "warning: type mismatch; saw type %d (%s)", file->type, file->name);
+        return false;
+    }
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "failed to read %s (%s)\n", file->filename, file->name);
+        return false;
+    }
+    psTrace ("psModules.camera", 5, "read %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
+    return true;
+}
+
+// create the data elements (headers, images) appropriate for this view
+bool pmFPAfileCreate (pmFPAfile *file, const pmFPAview *view, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    // these are not error conditions; these are state tests
+    if (file->state & PM_FPA_STATE_INACTIVE) {
+        psTrace("psModules.camera", 6, "skip create for inactive file %s", file->name);
+        return true;
+    }
+    if (file->mode != PM_FPA_MODE_WRITE) {
+        psTrace("psModules.camera", 6, "skip create for non-write file %s", file->name);
+        return true;
+    }
+
+    // an internal file should not be returned to here
+    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);
+
+    // get the current level
+    pmFPALevel level = pmFPAviewLevel (view);
+
+    // don't create the file if the src (FPA) is not defined
+    if (file->src == NULL) {
+        psTrace("psModules.camera", 6, "skip create for FPA without src FPA for %s", file->name);
+        return true;
+    }
+
+    // do we need to write this file?
+    if (level != file->fileLevel || file->mosaicLevel != PM_FPA_LEVEL_NONE) {
+        psTrace("psModules.camera", 6, "skip creation of %s at this level %s: fileLevel is %s",
+                file->name, pmFPALevelToName(level), pmFPALevelToName(file->fileLevel));
+        return true;
+    }
+
+    switch (file->type) {
+    case PM_FPA_FILE_IMAGE:
+    case PM_FPA_FILE_MASK:
+    case PM_FPA_FILE_WEIGHT:
+    case PM_FPA_FILE_FRINGE: {
+            // create FPA structure component based on view
+            psMetadata *format = file->format; // Camera format configuration
+            if (!format) {
+                format = config->format;
+            }
+
+            pmFPA *nameSource = file->src; // Source of FPA.NAME
+            if (!nameSource) {
+                nameSource = file->fpa;
+            }
+            bool mdok;                  // Status of MD lookup
+            const char *fpaname = psMetadataLookupStr(&mdok, nameSource->concepts, "FPA.NAME"); // Name of FPA
+
+            pmFPAAddSourceFromView(file->fpa, fpaname, view, format);
+            psTrace ("psModules.camera", 5, "created fpa data elements for %s (%s) (%d:%d:%d)\n", file->name, file->name, view->chip, view->cell, view->readout);
+            break;
+    }
+    case PM_FPA_FILE_HEADER:
+      psAbort ("Create not defined for HEADER");
+      break;
+    case PM_FPA_FILE_SX:
+    case PM_FPA_FILE_RAW:
+    case PM_FPA_FILE_OBJ:
+    case PM_FPA_FILE_CMP:
+    case PM_FPA_FILE_CMF:
+    case PM_FPA_FILE_PSF:
+    case PM_FPA_FILE_JPEG:
+    case PM_FPA_FILE_KAPA:
+        break;
+
+    default:
+        psError(PS_ERR_IO, true, "Unsupported type for %s: %d", file->name, file->type);
+        return false;
+    }
+    return true;
+}
+
+bool pmFPAfileWrite(pmFPAfile *file, const pmFPAview *view, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    if (file->state & PM_FPA_STATE_INACTIVE) {
+        psTrace("psModules.camera", 6, "skip write for %s, files is inactive", file->name);
+        return true;
+    }
+
+    if (file->mode != PM_FPA_MODE_WRITE) {
+        psTrace("psModules.camera", 6, "skip write for %s, mode is not WRITE", file->name);
+        return true;
+    }
+
+    // an internal file should not be returned to here
+    if (file->mode == PM_FPA_MODE_INTERNAL) {
+        psError(PS_ERR_IO, true, "File is mode PM_FPA_MODE_INTERNAL");
+        return false;
+    }
+
+    if (!file->save) {
+        psTrace("psModules.camera", 6, "skip write for %s, save is FALSE", file->name);
+        return true;
+    }
+
+    // get the current level
+    pmFPALevel level = pmFPAviewLevel (view);
+
+    // the effective dataLevel (for mosaics, we have preserved the original dataLevel)
+    pmFPALevel dataLevel = file->dataLevel;
+    if (file->mosaicLevel != PM_FPA_LEVEL_NONE && file->mosaicLevel < dataLevel) {
+        dataLevel = file->mosaicLevel;
+    }
+
+    // do we need to write this file?
+    if (level != dataLevel) {
+        psTrace("psModules.camera", 6, "skip writing of %s at this level %s: dataLevel is %s",
+                file->name, pmFPALevelToName(level), pmFPALevelToName(dataLevel));
+        return true;
+    }
+
+    // do we have data to write at this level?
+    if (!pmFPAviewCheckDataStatus (file->fpa, view)) {
+        psTrace("psModules.camera", 6, "skip write for %s, no data for this entry", file->name);
+        return true;
+    }
+
+    // note that for CMF and PSF, the test above is not sufficient to determine if there
+    // is actually any data to write out.  this is because these types of output files
+    // have their data stored on the readout->analysis metadata structure of another
+    // (existing) fpa
+    if (file->type == PM_FPA_FILE_PSF) {
+      if (!pmPSFmodelCheckDataStatusForView (view, file)) {
+        psTrace("psModules.camera", 6, "skip write for %s, no data for this entry", file->name);
+        return true;
+      }
+    }
+    if (file->type == PM_FPA_FILE_CMF) {
+      if (!pmFPAviewCheckDataStatusForSources (view, file)) {
+        psTrace("psModules.camera", 6, "skip write for %s, no data for this entry", file->name);
+        return true;
+      }
+    }
+
+    // open the file if not yet opened
+    // XXX do we need to test mosaicLevel?
+    if (level >= file->fileLevel) {
+        // we are allowed to open a file at a level which is not the fileLevel, but
+        // we need to supply view at the fileLevel for the file lookup functions below
+        pmFPAview *fileView = pmFPAviewForLevel (file->fileLevel, view);
+        if (!pmFPAfileOpen (file, fileView, config)) {
+            psError(PS_ERR_IO, false, "failed to open %s (%s)", file->filename, file->name);
+            psFree (fileView);
+            return false;
+        }
+
+        // do we need to write out a PHU?
+        if (!pmFPAfileWritePHU(file, fileView, config)) {
+            psError(PS_ERR_IO, false, "failed to write phu for %s (%s)", file->filename, file->name);
+            return false;
+        }
+
+        psFree (fileView);
+    }
+
+    if (file->compression) {
+        psTrace("psModules.camera", 7, "Setting compression for %s (%s)\n", file->filename, file->name);
+        if (!psFitsCompressionApply(file->fits, file->compression)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to set compression options for %s (%s) (%d:%d:%d)\n",
+                    file->filename, file->name, view->chip, view->cell, view->readout);
+            return false;
+        }
+    }
+
+
+    // Ensure headers and all are updated
+    // This is here so that the individual write functions (e.g., images, PSFs, sources, etc) don't have to
+    // take care of all this themselves (because they generally don't).
+    {
+        pmHDU *hdu = pmFPAviewThisHDU(view, file->fpa);
+        if (hdu) {
+            if (!hdu->header) {
+                hdu->header = psMetadataAlloc();
+            }
+            pmConfigConformHeader(hdu->header, file->format);
+        }
+
+        pmFPA *fpa = file->fpa;         // FPA of interest
+        pmChip *chip = pmFPAviewThisChip(view, file->fpa); // Chip of interest, or NULL
+        pmCell *cell = pmFPAviewThisCell(view, file->fpa); // Cell of interest, or NULL
+        pmFPAUpdateNames(fpa, chip, cell);
+
+        pmConceptSource sources = PM_CONCEPT_SOURCE_HEADER | PM_CONCEPT_SOURCE_CELLS |
+            PM_CONCEPT_SOURCE_DEFAULTS; // Concept sources to write
+        if (cell) {
+            if (!pmConceptsWriteCell(cell, sources, true, NULL)) {
+                psError(PS_ERR_IO, false, "Unable to write concepts for cell.\n");
+                return false;
+            }
+        } else if (chip) {
+            if (!pmConceptsWriteChip(chip, sources, true, true, NULL)) {
+                psError(PS_ERR_IO, false, "Unable to write concepts for chip.\n");
+                return false;
+            }
+        } else if (!pmConceptsWriteFPA(fpa, sources, true, NULL)) {
+            psError(PS_ERR_IO, false, "Unable to write concepts for FPA.\n");
+            return false;
+        }
+    }
+
+    // select a writing method
+    bool status = true;
+    switch (file->type) {
+      case PM_FPA_FILE_IMAGE:
+        status = pmFPAviewWriteFitsImage(view, file, config);
+        break;
+      case PM_FPA_FILE_MASK:
+        status = pmFPAviewWriteFitsMask(view, file, config);
+        break;
+      case PM_FPA_FILE_WEIGHT:
+        status = pmFPAviewWriteFitsWeight(view, file, config);
+        break;
+      case PM_FPA_FILE_HEADER:
+        psAbort ("no HEADER write functions defined");
+        break;
+      case PM_FPA_FILE_FRINGE:
+        status = pmFPAviewWriteFitsImage (view, file, config);
+        if (status) {
+            if (!pmFPAviewWriteFitsTable(view, file, "FRINGE")) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to write fringe data from %s.\n", file->filename);
+                return false;
+            }
+        }
+        break;
+      case PM_FPA_FILE_SX:
+      case PM_FPA_FILE_RAW:
+      case PM_FPA_FILE_OBJ:
+      case PM_FPA_FILE_CMP:
+      case PM_FPA_FILE_CMF:
+        status = pmFPAviewWriteObjects (view, file, config);
+        break;
+
+      case PM_FPA_FILE_PSF:
+        status = pmPSFmodelWriteForView (view, file, config);
+        break;
+
+      case PM_FPA_FILE_JPEG:
+        status = pmFPAviewWriteJPEG (view, file, config);
+        break;
+
+      case PM_FPA_FILE_KAPA:
+        status = pmFPAviewWriteSourcePlot (view, file, config);
+        break;
+
+      default:
+        psError(PS_ERR_IO, true, "warning: type mismatch; saw type %d (%s)", file->type, file->name);
+        return false;
+    }
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "failed to write %s (%s)\n", file->filename, file->name);
+        return false;
+    }
+    psTrace ("psModules.camera", 5, "wrote %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
+    return true;
+}
+
+bool pmFPAfileClose (pmFPAfile *file, const pmFPAview *view)
+{
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    // an internal file should not be sent here (should not be left on config->files)
+    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);
+
+    // skip the following states
+    if (file->state & PM_FPA_STATE_INACTIVE) {
+        psTrace("psModules.camera", 6, "skip close for %s, files is inactive", file->name);
+        return true;
+    }
+    if (file->state == PM_FPA_STATE_CLOSED) {
+        psTrace("psModules.camera", 6, "skip close for %s, files is closed", file->name);
+        return true;
+    }
+
+    // is current level == open level?
+    pmFPALevel level = pmFPAviewLevel (view);
+    if (file->fileLevel != level) {
+        psTrace("psModules.camera", 6, "skip closing of %s at this level %s: fileLevel is %s",
+                file->name, pmFPALevelToName(level), pmFPALevelToName(file->fileLevel));
+        return true;
+    }
+
+    // check if we are actually open
+    bool status = true;
+    switch (file->type) {
+        // check the FITS types
+      case PM_FPA_FILE_IMAGE:
+      case PM_FPA_FILE_MASK:
+      case PM_FPA_FILE_WEIGHT:
+      case PM_FPA_FILE_HEADER:
+      case PM_FPA_FILE_FRINGE:
+      case PM_FPA_FILE_CMF:
+      case PM_FPA_FILE_PSF:
+        psTrace ("psModules.camera", 5, "closing %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
+        status = psFitsClose (file->fits);
+        file->fits = NULL;
+        file->header = NULL;
+        file->state = PM_FPA_STATE_CLOSED;
+        file->wrote_phu = false;
+        break;
+
+        // ignore the TEXT types
+      case PM_FPA_FILE_SX:
+      case PM_FPA_FILE_RAW:
+      case PM_FPA_FILE_OBJ:
+      case PM_FPA_FILE_CMP:
+      case PM_FPA_FILE_JPEG:
+      case PM_FPA_FILE_KAPA:
+        break;
+
+      default:
+        psError(PS_ERR_IO, true, "type mismatch: %d (%s)", file->type, file->name);
+        return false;
+    }
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "failed to close %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
+        return false;
+    }
+    return true;
+}
+
+bool pmFPAfileFreeData(pmFPAfile *file, const pmFPAview *view)
+{
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    // an internal file should not be sent here (should not be left on config->files)
+    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);
+
+    if (file->state & PM_FPA_STATE_INACTIVE) {
+        psTrace("psModules.camera", 6, "skip free for %s, files is inactive", file->name);
+        return true;
+    }
+
+    // get the current level
+    pmFPALevel level = pmFPAviewLevel (view);
+
+    // do we need to free this file?
+    if (level != file->freeLevel) {
+        psTrace("psModules.camera", 6, "skip free of %s at this level %s: freeLevel is %s",
+                file->name, pmFPALevelToName(level), pmFPALevelToName(file->freeLevel));
+        return true;
+    }
+
+    bool status = true;
+    switch (file->type) {
+      case PM_FPA_FILE_IMAGE:
+      case PM_FPA_FILE_MASK:
+      case PM_FPA_FILE_WEIGHT:
+      case PM_FPA_FILE_HEADER:
+      case PM_FPA_FILE_FRINGE:
+        status = pmFPAviewFreeData(view, file);
+        break;
+      case PM_FPA_FILE_SX:
+      case PM_FPA_FILE_RAW:
+      case PM_FPA_FILE_OBJ:
+      case PM_FPA_FILE_CMP:
+      case PM_FPA_FILE_CMF:
+      case PM_FPA_FILE_PSF:
+        psTrace ("psModules.camera", 5, "NOT freeing %s (%s) : save for further analysis\n", file->filename, file->name);
+        return true;
+      case PM_FPA_FILE_JPEG:
+      case PM_FPA_FILE_KAPA:
+        psTrace ("psModules.camera", 5, "nothing to free for %s (%s)\n", file->filename, file->name);
+        return true;
+      default:
+        psError(PS_ERR_IO, true, "warning: type mismatch; saw type %d", file->type);
+        return false;
+    }
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "failed to read %s (%s)\n", file->filename, file->name);
+        return false;
+    }
+    psTrace ("psModules.camera", 5, "freed %s (%s) (%d:%d:%d)\n", file->filename, file->name, view->chip, view->cell, view->readout);
+    return true;
+}
+
+// open file (if not already opened).
+// this function is only called only within pmFPAfileRead or pmFPAfileWrite.
+bool pmFPAfileOpen (pmFPAfile *file, const pmFPAview *view, pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    bool status;
+    char *mode = NULL;
+    char *readMode = "r";
+    char *writeMode = "w";
+
+    if (file->state & PM_FPA_STATE_INACTIVE) {
+        psTrace("psModules.camera", 6, "skip open for %s, files is inactive", file->name);
+        return true;
+    }
+
+    if (file->state == PM_FPA_STATE_OPEN) {
+        return true;
+    }
+
+    // these are programming errors
+    PS_ASSERT(file->mode != PM_FPA_MODE_NONE, false);
+    PS_ASSERT(file->mode != PM_FPA_MODE_INTERNAL, false);
+
+    if (file->mode == PM_FPA_MODE_READ) {
+        mode = readMode;
+    }
+    if (file->mode == PM_FPA_MODE_WRITE) {
+        mode = writeMode;
+    }
+    if ((file->mode == PM_FPA_MODE_WRITE) && !file->save) {
+        psTrace("psModules.camera", 6, "skip open for %s, output file not requested", file->name);
+        return true;
+    }
+
+    // determine the file name, free a name allocated earlier
+    psFree (file->filename);
+    file->filename = pmFPAfileNameFromRule (file->filerule, file, view);
+    if (file->filename == NULL) {
+        psError(PS_ERR_IO, true, "Filename is NULL");
+        return false;
+    }
+
+    // indirect filenames: these come from a list on the command line or elsewhere
+    if (!strcasecmp (file->filename, "@FILES")) {
+        char *filesrc = pmFPAfileNameFromRule (file->filesrc, file, view);
+        if (filesrc == NULL) {
+            psError(PS_ERR_IO, false, "error converting filesrc to name %s\n", file->filesrc);
+            return false;
+        }
+
+        psFree (file->filename);
+        file->filename = psMetadataLookupStr (&status, file->names, filesrc);
+
+        if (file->filename == NULL) {
+            psError(PS_ERR_IO, true, "filename lookup error (@FILES) for %s : %s\n", file->filesrc, filesrc);
+            psFree (filesrc);
+            return false;
+        }
+        // psMetadataLookupStr just returns a view, file->filename must be protected
+        psMemIncrRefCounter (file->filename);
+        psFree (filesrc);
+    }
+
+    // get name from detrend database
+    // file->detrend->detID contains the desired -det_id detID -iteration iter string
+    if (!strcasecmp (file->filename, "@DETDB")) {
+        if (!file->detrend) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find information about selected detrend.");
+            return false;
+        }
+
+        psString classId = NULL;        // The class identifier, to pass to pmDetrendFile
+        psMetadata *menu = psMetadataLookupMetadata(&status, file->camera, "CLASSID"); // Menu of class IDs
+        if (!status || !menu) {
+            psWarning("Unable to find CLASSID metadata in camera configuration --- "
+                      "assuming no class identifier required.");
+        } else {
+            const char *rule = psMetadataLookupStr(&status, menu, file->detrend->level); // Rule for class_id
+            if (!status || !rule || strlen(rule) == 0) {
+                psWarning("Unable to find %s in CLASSID within camera configuration --- "
+                          "attempting to proceed without it.", file->detrend->level);
+            } else {
+                classId = pmFPAfileNameFromRule(rule, file, view);
+                if (!classId) {
+                    psError(PS_ERR_IO, false, "error converting CLASSID rule to name: %s\n", rule);
+                    return false;
+                }
+            }
+        }
+        psTrace ("psModules.camera", 6, "looking for detrend (%s, %s)\n", file->detrend->detID, classId);
+        psFree (file->filename);
+
+        file->filename = pmDetrendFile(file->detrend->detID, classId, config);
+        if (file->filename == NULL) {
+            psError(PS_ERR_IO, false, "failed to find a valid detrend image for detID %s : classID %s\n", file->detrend->detID, classId);
+            psFree (classId);
+            return false;
+        }
+
+        psTrace ("psModules.camera", 6, "got detrend file %s\n", file->filename);
+        psFree (classId);
+    }
+
+    // apply filename mangling rules (file://, path://, neb://)
+    bool create = file->mode == PM_FPA_MODE_WRITE ? true : false;
+    psString tmpName = pmConfigConvertFilename (file->filename, config, create);
+    psFree (file->filename);
+    file->filename = tmpName;
+
+    switch (file->type) {
+        // open the FITS types:
+      case PM_FPA_FILE_IMAGE:
+      case PM_FPA_FILE_MASK:
+      case PM_FPA_FILE_WEIGHT:
+      case PM_FPA_FILE_HEADER:
+      case PM_FPA_FILE_FRINGE:
+      case PM_FPA_FILE_CMF:
+      case PM_FPA_FILE_PSF:
+        psTrace ("psModules.camera", 5, "opening %s (%s) (%d:%d:%d)\n",
+                 file->filename, file->name, view->chip, view->cell, view->readout);
+        file->fits = psFitsOpen (file->filename, mode);
+        if (file->fits == NULL) {
+            psError(PS_ERR_IO, false, "error opening file %s\n", file->filename);
+            return false;
+        }
+        file->state = PM_FPA_STATE_OPEN;
+
+        // if needed, set the optional EXTWORD field based on the camera value
+        if (file->format) {
+            psMetadata *fileMenu = psMetadataLookupMetadata (NULL, file->format, "FILE");
+            if (!fileMenu) {
+                psError (PS_ERR_IO, true, "FILE METADATA missing from camera format %s\n",
+                         config->formatName);
+                return false;
+            }
+            char *extword = psMetadataLookupStr (&status, fileMenu, "EXTWORD");
+            if (status) {
+                psFitsSetExtnameWord (file->fits, extword);
+            }
+        }
+
+        if (file->compression) {
+            psTrace("psModules.camera", 7, "Setting compression for %s (%s)\n", file->filename, file->name);
+            if (!psFitsCompressionApply(file->fits, file->compression)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to set compression options for %s (%s) (%d:%d:%d)\n",
+                        file->filename, file->name, view->chip, view->cell, view->readout);
+                return false;
+            }
+        }
+
+        // In some cases, we need to read the PHU after we've opened the file.  This happens for the images
+        // supplied by the detrend database, which are only identified here (pmConfigConvertFilename).
+        if (!pmFPAfileReadPHU (file, view, config)) {
+            psError (PS_ERR_IO, true, "error reading PHU for %s (%s) (%d:%d:%d)\n",
+                     file->filename, file->name, view->chip, view->cell, view->readout);
+            return false;
+        }
+        break;
+
+        // defer opening TEXT types:
+      case PM_FPA_FILE_SX:
+      case PM_FPA_FILE_OBJ:
+      case PM_FPA_FILE_CMP:
+      case PM_FPA_FILE_RAW:
+      case PM_FPA_FILE_JPEG:
+      case PM_FPA_FILE_KAPA:
+        psTrace ("psModules.camera", 5, "defer opening %s\n", file->filename);
+        break;
+
+      default:
+        psError(PS_ERR_IO, true, "type mismatch for %s : %d\n", file->filename, file->type);
+        return false;
+    }
+    return true;
+}
+
+// for this file and view, if we need to read a PHU, read it.  return true for any non-error
+// condition. this function should be called by pmFPAfileOpen.
+bool pmFPAfileReadPHU (pmFPAfile *file, const pmFPAview *view, pmConfig *config)
+{
+    // required conditions
+    if (file->mode != PM_FPA_MODE_READ) return true;
+    if (file->state != PM_FPA_STATE_OPEN) psAbort ("pmFPAfileReadPHU called on unopened file");
+    if (file->fpa == NULL) psAbort ("pmFPAfileReadPHU called on file without an FPA");
+
+    // check if we need to read a PHU (if not, return true)
+    switch (file->fileLevel) {
+      case PM_FPA_LEVEL_FPA:
+        if (file->fpa->hdu) return true;
+        break;
+      case PM_FPA_LEVEL_CHIP: {
+          pmChip *chip = pmFPAviewThisChip(view, file->fpa);
+          if (!chip) psAbort ("inconsistent file/fpa: fileLevel is CHIP, view is FPA");
+          if (chip->hdu) return true;
+          break;
+      }
+      case PM_FPA_LEVEL_CELL: {
+          pmCell *cell = pmFPAviewThisCell(view, file->fpa);
+          if (!cell) psAbort ("inconsistent file/fpa: fileLevel is CELL, view is FPA");
+          if (cell->hdu) return true;
+          break;
+      }
+      case PM_FPA_LEVEL_NONE:
+        // Might get here immediately after opening a file selected from the detrend database.
+        break;
+      default:
+        psAbort("fileLevel not correctly set");
+        break;
+    }
+
+    // XXX do we need to advance to the first HDU?
+    psMetadata *phu = psFitsReadHeader (NULL, file->fits);
+    if (!file->format) {
+        // XXX do we need to read the recipe here?  these files are supplemental, and probably
+        // do not need to re-load the recipes
+        file->format = pmConfigCameraFormatFromHeader (config, phu, false);
+        if (!file->format) {
+            psError(PS_ERR_IO, false, "Failed to read CCD format from %s\n", file->filename);
+            psFree(phu);
+            return false;
+        }
+        file->formatName = psStringCopy(config->formatName);
+
+    } else {
+        bool valid;
+        if (!pmConfigValidateCameraFormat (&valid, file->format, phu)) {
+            psError (PS_ERR_UNKNOWN, false, "Error in camera configuration\n");
+            psFree (phu);
+            return false;
+        }
+        if (!valid) {
+            psError(PS_ERR_IO, false, "file %s is not from the required camera", file->filename);
+            psFree (phu);
+            return false;
+        }
+    }
+    pmFPAview *thisView = pmFPAAddSourceFromHeader (file->fpa, phu, file->format);
+    assert (thisView); // XXX we are having some trouble with input psf files not having the Cell and fpa names matching.
+    psFree (thisView);
+    psFree (phu);
+    // XXX we can check the output view to be sure it corresponds to our current view
+    return true;
+}
+
+// XXX this function is only called from pmFPAfileWrite
+// XXX for each data type, there should be a function which writes the PHU, if needed
+bool pmFPAfileWritePHU(pmFPAfile *file, const pmFPAview *view, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    if (file->wrote_phu) return true;
+
+    bool status = true;
+    switch (file->type) {
+      case PM_FPA_FILE_IMAGE:
+      case PM_FPA_FILE_MASK:
+      case PM_FPA_FILE_WEIGHT:
+      case PM_FPA_FILE_FRINGE:
+        status = pmFPAviewFitsWritePHU (view, file, config);
+        break;
+      case PM_FPA_FILE_CMF:
+        status = pmSource_CMF_WritePHU (view, file, config);
+        break;
+      case PM_FPA_FILE_PSF:
+        status = pmPSFmodelWritePHU (view, file, config);
+        break;
+      case PM_FPA_FILE_SX:
+      case PM_FPA_FILE_RAW:
+      case PM_FPA_FILE_OBJ:
+      case PM_FPA_FILE_CMP:
+      case PM_FPA_FILE_JPEG:
+      case PM_FPA_FILE_KAPA:
+        break;
+      default:
+        fprintf (stderr, "warning: type mismatch\n");
+        return false;
+    }
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "failed to write PHU for %s (%s)\n", file->filename, file->name);
+        return false;
+    }
+    // XXX this is also being set in the individual functions.  choose one or the other
+    file->wrote_phu = true;
+    return true;
+}
+
+// set the state of the specified pmFPAfile to active (state == true) or inactive
+// if name is NULL, set the state for all pmFPAfiles
+bool pmFPAfileActivate (psMetadata *files, bool state, char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(files, false);
+
+    // Do this as an iteration rather than a lookup, since we may want to turn on/off multiple files at once
+    // (if there are multiple files defined with the same name).
+
+    if (!name) {
+        psMetadataItem *item = NULL;
+        psMetadataIterator *iter = psMetadataIteratorAlloc (files, PS_LIST_HEAD, NULL);
+        while ((item = psMetadataGetAndIncrement (iter)) != NULL) {
+            pmFPAfile *file = item->data.V;
+            if (state) {
+                file->state &= PS_NOT_U8(PM_FPA_STATE_INACTIVE);
+            } else {
+                file->state |= PM_FPA_STATE_INACTIVE;
+            }
+        }
+        psFree (iter);
+        return true;
+    }
+
+    if (!psMetadataLookup(files, name)) {
+        // We activated every example of that file that we could find.
+        return true;
+    }
+
+    psString regex = NULL;              // Regular expression for psMetadataIteratorAlloc
+    if (name) {
+        psStringAppend(&regex, "^%s$", name);
+    }
+
+    int num = 0;                        // Number of files activated
+    psMetadataItem *item = NULL;        // Item from iteration
+    psMetadataIterator *iter = psMetadataIteratorAlloc(files, PS_LIST_HEAD, regex); // Iterator
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        pmFPAfile *file = item->data.V; // File of interest
+        if (state) {
+            file->state &= PS_NOT_U8(PM_FPA_STATE_INACTIVE);
+        } else {
+            file->state |= PM_FPA_STATE_INACTIVE;
+        }
+        num++;
+    }
+    psFree(iter);
+    psFree(regex);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileIO.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileIO.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAfileIO.h	(revision 15358)
@@ -0,0 +1,45 @@
+/* @file  pmFPAview.h
+ * @brief Tools to manipulate the FPA structure elements.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-04-14 03:22:47 $
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_FILE_IO_H
+#define PM_FPA_FILE_IO_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+// open the real file corresponding to the given pmFPAfile appropriate to the current view
+bool pmFPAfileOpen (pmFPAfile *file, const pmFPAview *view, pmConfig *config);
+
+// read from the real file corresponding to the given pmFPAfile for the current view
+bool pmFPAfileRead (pmFPAfile *file, const pmFPAview *view, pmConfig *config);
+
+bool pmFPAfileCreate (pmFPAfile *file, const pmFPAview *view, const pmConfig *config);
+
+// write to the real file corresponding to the given pmFPAfile for the current view
+bool pmFPAfileWrite (pmFPAfile *file, const pmFPAview *view, pmConfig *config);
+
+// close the real file corresponding to the given pmFPAfile appropriate to the current view
+bool pmFPAfileClose (pmFPAfile *file, const pmFPAview *view);
+
+// free the data at this level
+bool pmFPAfileFreeData(pmFPAfile *file, const pmFPAview *view);
+
+// set the state of the specified pmFPAfile to active (state == true) or inactive
+// if name is NULL, set the state for all pmFPAfiles
+bool pmFPAfileActivate (psMetadata *files, bool state, char *name);
+
+// examine all pmFPAfiles listed in the files and perform the needed I/O operations (open,read,write,close)
+bool pmFPAfileIOChecks (pmConfig *config, const pmFPAview *view, pmFPAfilePlace place);
+
+bool pmFPAfileWritePHU(pmFPAfile *file, const pmFPAview *view, const pmConfig *config);
+bool pmFPAfileReadPHU (pmFPAfile *file, const pmFPAview *view, pmConfig *config);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAview.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAview.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAview.c	(revision 15358)
@@ -0,0 +1,368 @@
+/** @file  pmFPAview.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-29 01:26:55 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmHDUUtils.h"
+#include "pmFPAview.h"
+
+static void pmFPAviewFree(pmFPAview *view)
+{
+    // No reason to keep this function, apart from the fact that it allows us to type the memBlock
+    return;
+}
+
+pmFPAview *pmFPAviewAlloc(int nRows)
+{
+    pmFPAview *view = psAlloc(sizeof(pmFPAview));
+    psMemSetDeallocator(view, (psFreeFunc) pmFPAviewFree);
+
+    view->nRows = nRows;
+    pmFPAviewReset(view);
+    return view;
+}
+
+
+bool pmFPAviewReset(pmFPAview *view)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    view->chip    = -1;
+    view->cell    = -1;
+    view->readout = -1;
+    view->iRows   =  0;
+    return true;
+}
+
+// return a view restricted to the level (must be >= the input level)
+pmFPAview *pmFPAviewForLevel(pmFPALevel level, const pmFPAview *input)
+{
+    PS_ASSERT_PTR_NON_NULL(input, NULL);
+
+    pmFPAview *output = pmFPAviewAlloc (input->nRows);
+    *output = *input;
+
+    switch (level) {
+      case PM_FPA_LEVEL_FPA:
+        output->chip = -1;
+      case PM_FPA_LEVEL_CHIP:
+        output->cell = -1;
+      case PM_FPA_LEVEL_CELL:
+        output->readout = -1;
+        break;
+      default:
+        break;
+    }
+    return output;
+}
+
+pmFPALevel pmFPAviewLevel(const pmFPAview *view)
+{
+    PS_ASSERT_PTR_NON_NULL(view, PM_FPA_LEVEL_NONE);
+
+    if (view->chip < 0) {
+        return PM_FPA_LEVEL_FPA;
+    }
+    if (view->cell < 0) {
+        return PM_FPA_LEVEL_CHIP;
+    }
+    if (view->readout < 0) {
+        return PM_FPA_LEVEL_CELL;
+    }
+    return PM_FPA_LEVEL_READOUT;
+}
+
+pmChip *pmFPAviewThisChip(const pmFPAview *view, const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa->chips, NULL);
+
+    if (view->chip < 0) {
+        return NULL;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return NULL;
+    }
+
+    pmChip *chip = fpa->chips->data[view->chip];
+    return chip;
+}
+
+pmChip *pmFPAviewNextChip(pmFPAview *view, const pmFPA *fpa, int nStep)
+{
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    view->cell = -1;
+    view->readout = -1;
+    view->iRows = 0;
+
+    // if there are no available chips, return NULL
+    if (fpa->chips->n <= 0) {
+        view->chip = -1;
+        return NULL;
+    }
+
+    // clean up < -1 values
+    if (view->chip < -1) {
+        view->chip = -1;
+    }
+
+    // increment to the next chip
+    view->chip += nStep;
+
+    // if we are at the end of the stack, return NULL
+    if (view->chip >= fpa->chips->n) {
+        view->chip = -1;
+        return NULL;
+    }
+
+    // get the correct chip pointer
+    pmChip *chip = fpa->chips->data[view->chip];
+    return (chip);
+}
+
+pmCell *pmFPAviewThisCell(const pmFPAview *view, const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    if (view->cell < 0) {
+        return NULL;
+    }
+
+    pmChip *chip = pmFPAviewThisChip (view, fpa);
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    if (view->cell >= chip->cells->n) {
+        return NULL;
+    }
+
+    pmCell *cell = chip->cells->data[view->cell];
+    return cell;
+}
+
+pmCell *pmFPAviewNextCell (pmFPAview *view, const pmFPA *fpa, int nStep)
+{
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    pmChip *chip = pmFPAviewThisChip (view, fpa);
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    view->readout = -1;
+    view->iRows = 0;
+
+    // if there are no available cells, return NULL
+    if (chip->cells->n <= 0) {
+        view->cell = -1;
+        return NULL;
+    }
+
+    // clean up < -1 values
+    if (view->cell < -1) {
+        view->cell = -1;
+    }
+
+    // increment to the next cell
+    view->cell += nStep;
+
+    // if we are at the end of the stack, return NULL
+    if (view->cell >= chip->cells->n) {
+        view->cell = -1;
+        return NULL;
+    }
+
+    // get the correct cell pointer
+    pmCell *cell = chip->cells->data[view->cell];
+    return (cell);
+}
+
+pmReadout *pmFPAviewThisReadout (const pmFPAview *view, const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    if (view->readout < 0) {
+        return NULL;
+    }
+
+    pmCell *cell = pmFPAviewThisCell (view, fpa);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+
+    if (view->readout >= cell->readouts->n) {
+        return NULL;
+    }
+
+    pmReadout *readout = cell->readouts->data[view->readout];
+    return readout;
+}
+
+pmReadout *pmFPAviewNextReadout (pmFPAview *view, const pmFPA *fpa, int nStep)
+{
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    pmCell *cell = pmFPAviewThisCell (view, fpa);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+
+    view->iRows = 0;
+
+    // if there are no available cells, return NULL
+    if (cell->readouts->n <= 0) {
+        view->readout = -1;
+        return NULL;
+    }
+
+    // clean up < -1 values
+    if (view->readout < -1) {
+        view->readout = -1;
+    }
+
+    // increment to the next cell
+    view->readout += nStep;
+
+    // if we are at the end of the stack, return NULL
+    if (view->readout >= cell->readouts->n) {
+        view->readout = -1;
+        return NULL;
+    }
+
+    // get the correct cell pointer
+    pmReadout *readout = cell->readouts->data[view->readout];
+    return (readout);
+}
+
+pmHDU *pmFPAviewThisHDU(const pmFPAview *view, const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    // the HDU is attached to a cell, chip or fpa
+    // if this view has a -1 for the level which contains the hdu,
+    // there is no unambiguous HDU
+
+    if (view->chip < 0) {
+        return pmHDUFromFPA (fpa);
+    }
+    if (view->cell < 0) {
+        return pmHDUFromChip (pmFPAviewThisChip (view, fpa));
+    }
+    if (view->readout < 0) {
+        return pmHDUFromCell (pmFPAviewThisCell (view, fpa));
+    }
+    return pmHDUFromReadout (pmFPAviewThisReadout (view, fpa));
+}
+
+pmHDU *pmFPAviewThisPHU(const pmFPAview *view, const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    // select the HDU which corresponds to the PHU containing this view
+
+    pmHDU *hdu;
+    pmFPAview new;
+    pmChip *chip;
+    pmCell *cell;
+
+    new = *view;
+
+    if (view->chip < 0) {
+        hdu = pmHDUFromFPA (fpa);
+        if (!hdu)
+            return NULL;
+        if (hdu->blankPHU)
+            return hdu;
+        return NULL;
+    }
+    if (view->cell < 0) {
+        chip = pmFPAviewThisChip (view, fpa);
+        hdu  = pmHDUFromChip (chip);
+        if (!hdu)
+            return NULL;
+        if (hdu->blankPHU)
+            return hdu;
+        new.chip = -1;
+        hdu = pmFPAviewThisPHU (&new, fpa);
+        return hdu;
+    }
+    if (view->readout < 0) {
+        cell = pmFPAviewThisCell (view, fpa);
+        hdu  = pmHDUFromCell (cell);
+        if (!hdu) {
+            psAbort("a split readout is not covered by the current paradigm");
+        }
+        if (hdu->blankPHU)
+            return hdu;
+        new.cell = -1;
+        hdu = pmFPAviewThisPHU (&new, fpa);
+        return hdu;
+    }
+    return NULL;
+}
+
+pmFPAview *pmFPAviewGenerate(const pmFPA *fpa, const pmChip *chip, const pmCell *cell,
+                             const pmReadout *readout)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    pmFPAview *view = pmFPAviewAlloc(0);// View to return
+
+    if (!chip) {
+        return view;
+    }
+
+    for (view->chip = 0; view->chip < fpa->chips->n && fpa->chips->data[view->chip] != chip; view->chip++);
+    if (view->chip == fpa->chips->n) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find chip %p in fpa.", chip);
+        psFree(view);
+        return NULL;
+    }
+
+    if (!cell) {
+        return view;
+    }
+
+    for (view->cell = 0; view->cell < chip->cells->n && chip->cells->data[view->cell] != cell; view->cell++);
+    if (view->cell == chip->cells->n) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find cell %p in chip.", cell);
+        psFree(view);
+        return NULL;
+    }
+
+    if (!readout) {
+        return view;
+    }
+
+    for (view->readout = 0;
+         view->readout < cell->readouts->n && cell->readouts->data[view->readout] != readout;
+         view->readout++);
+    if (view->readout == cell->readouts->n) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find readout %p in cell.", readout);
+        psFree(view);
+        return NULL;
+    }
+
+    return view;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/camera/pmFPAview.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmFPAview.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmFPAview.h	(revision 15358)
@@ -0,0 +1,127 @@
+/* @file pmFPA.h
+ * @brief Tools to manipulate the FPA structure elements.
+ *
+ * @author Eugene Magnier, IfA
+ *
+ * @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-09-29 01:19:47 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FPA_VIEW_H
+#define PM_FPA_VIEW_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+// #include "pmFPA.h"
+// #include "pmFPALevel.h"
+
+/// Identifier for FPA components
+///
+/// This structure allows the identification of a single component of the focal plane hierarchy (or multiple,
+/// if we consider selecting all those components below the selected component).  Components are identified on
+/// the basis of their chip, cell, readout index.  An index of -1 means all components at that level.
+/// Additionally, since readouts may be read piecemeal, there are additional indices for these.
+typedef struct
+{
+    int chip;                           ///< Number of the chip, or -1 for all
+    int cell;                           ///< Number of the cell, or -1 for all
+    int readout;                        ///< Number of the readout, or -1 for all
+    int nRows;                          ///< Maximum number of rows per readout segment read, or 0 for all
+    int iRows;                          ///< Starting point for this read
+}
+pmFPAview;
+
+/// Allocator for pmFPAview
+pmFPAview *pmFPAviewAlloc(int nRows     ///< Maximum number of rows per readout segment read, or 0 for all
+                         );
+
+/// Reset a view to select all components
+bool pmFPAviewReset(pmFPAview *view     ///< View to reset
+                   );
+
+// return a view restricted to the level (must be >= the input level)
+pmFPAview *pmFPAviewForLevel(pmFPALevel level, const pmFPAview *input);
+
+/// Determine the current view level
+///
+/// Returns the level appropriate for the view
+pmFPALevel pmFPAviewLevel(const pmFPAview *view ///< View to examine
+                         );
+
+// Lookups
+
+/// Return the currently selected chip for this view
+///
+/// Returns NULL if the selection is not specific or invalid
+pmChip *pmFPAviewThisChip(const pmFPAview *view, ///< Current view
+                          const pmFPA *fpa ///< FPA containing chip
+                         );
+
+/// Return the currently selected cell for this view
+///
+/// Returns NULL if the selection is not specific or invalid
+pmCell *pmFPAviewThisCell(const pmFPAview *view, ///< Current view
+                          const pmFPA *fpa ///< FPA containing cell
+                         );
+
+/// Return the currently selected readout for this view
+///
+/// Returns NULL if the selection is not specific or invalid
+pmReadout *pmFPAviewThisReadout(const pmFPAview *view, ///< Current view
+                                const pmFPA *fpa ///< FPA containing readout
+                               );
+
+// Incrementors
+
+/// Advance view to the next chip
+///
+/// Returns NULL if there is no next
+pmChip *pmFPAviewNextChip(pmFPAview *view, ///< Current view
+                          const pmFPA *fpa, ///< FPA containing chips
+                          int nStep     ///< Number of chips to increment
+                         );
+
+/// Advance view to the next cell
+///
+/// Returns NULL if there is no next
+pmCell *pmFPAviewNextCell(pmFPAview *view, ///< Current view
+                          const pmFPA *fpa, ///< FPA containing cells
+                          int nStep     ///< Number of cells to increment
+                         );
+
+/// Advance view to the next readout
+///
+/// Returns NULL if there is no next
+pmReadout *pmFPAviewNextReadout(pmFPAview *view, ///< Current view
+                                const pmFPA *fpa, ///< FPA containing readouts
+                                int nStep ///< Number of readouts to increment
+                               );
+
+/// Return the HDU corresponding to the current view
+///
+/// Uses the pmHDUFrom* functions, combined with the view.
+pmHDU *pmFPAviewThisHDU(const pmFPAview *view, ///< Current view
+                        const pmFPA *fpa ///< FPA for view
+                       );
+
+/// Return the blank Primary HDU corresponding to the current view, if any
+///
+/// Similar to pmFPAviewThisHDU, except returns NULL if no HDU is found, or the HDU is not a blank Primary HDU
+pmHDU *pmFPAviewThisPHU(const pmFPAview *view, ///< Current view
+                        const pmFPA *fpa ///< FPA for view
+                       );
+
+/// Generate a view, given a chip, cell, readout.
+///
+/// Uses the pointer value in the array of the parent to locate the child
+pmFPAview *pmFPAviewGenerate(const pmFPA *fpa, ///< FPA of interest
+                             const pmChip *chip, ///< Chip of interest, or NULL
+                             const pmCell *cell, ///< Cell of interest, or NULL
+                             const pmReadout *reaodut ///< Readout of interest, or NULL
+    );
+
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmHDU.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmHDU.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmHDU.c	(revision 15358)
@@ -0,0 +1,231 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static (private) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Move to the appropriate extension in FITS file for HDU
+static bool hduMove(pmHDU *hdu,         // HDU with extname
+                    psFits *fits        // FITS file in which to move
+                   )
+{
+    // Deal with the PHU case
+    if (hdu->blankPHU || !hdu->extname) {
+        if (!psFitsMoveExtNum(fits, 0, false)) {
+            psError(PS_ERR_IO, false, "Unable to move to primary header!\n");
+            return false;
+        }
+        return true;
+    }
+
+    if (!psFitsMoveExtName(fits, hdu->extname)) {
+        psError(PS_ERR_IO, false, "Unable to move to extension %s\n", hdu->extname);
+        return false;
+    }
+
+    return true;
+}
+
+static void hduFree(pmHDU *hdu)
+{
+    psFree(hdu->extname);
+    psFree(hdu->format);
+    psFree(hdu->header);
+    psFree(hdu->images);
+    psFree(hdu->weights);
+    psFree(hdu->masks);
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+pmHDU *pmHDUAlloc(const char *extname)
+{
+    pmHDU *hdu = psAlloc(sizeof(pmHDU));
+    psMemSetDeallocator(hdu, (psFreeFunc)hduFree);
+
+    if (!extname || strlen(extname) == 0) {
+        hdu->blankPHU = true;
+        hdu->extname = NULL;
+    } else {
+        hdu->blankPHU = false;
+        hdu->extname = psStringCopy(extname);
+    }
+    hdu->format  = NULL;
+    hdu->header  = NULL;
+    hdu->images  = NULL;
+    hdu->weights = NULL;
+    hdu->masks   = NULL;
+
+    return hdu;
+}
+
+bool pmHDUReadHeader(pmHDU *hdu, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    // Move to the appropriate extension
+    psTrace("psModules.camera", 5, "Moving to extension %s...\n", hdu->extname);
+    if (!hduMove(hdu, fits)) {
+        return false;
+    }
+
+    if (!hdu->header) {
+        psTrace("psModules.camera", 5, "Reading the header...\n");
+        hdu->header = psFitsReadHeader(hdu->header, fits);
+        if (! hdu->header) {
+            psError(PS_ERR_IO, false, "Unable to read header for extension %s\n", hdu->extname);
+            return false;
+        }
+    }
+
+    return true;
+}
+
+// Read an HDU from a FITS file
+// XXX: Add a region specifier?
+bool hduRead(pmHDU *hdu,                // HDU to write
+             psArray **images,          // Images into which to read
+             psFits *fits               // FITS file to read
+            )
+{
+    assert(hdu);
+    assert(images);
+    assert(fits);
+
+    // Read the header; includes the move
+    if (!pmHDUReadHeader(hdu, fits)) {
+        return false;
+    }
+
+    if (hdu->blankPHU) {
+        // Done already!
+        return true;
+    }
+
+    if (*images) {
+        psLogMsg(__func__, PS_LOG_WARN, "HDU %s has already been read --- overwriting.\n", hdu->extname);
+        psFree(*images);                // Blow away anything existing
+    }
+    psTrace("psModules.camera", 5, "Reading the pixels...\n");
+    *images = psFitsReadImageCube(fits, psRegionSet(0,0,0,0));
+    if (!*images) {
+        psError(PS_ERR_IO, false, "Unable to read pixels for extension %s\n", hdu->extname);
+        return false;
+    }
+    return true;
+}
+
+bool pmHDURead(pmHDU *hdu, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return hduRead(hdu, &hdu->images, fits);
+}
+
+bool pmHDUReadMask(pmHDU *hdu, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return hduRead(hdu, &hdu->masks, fits);
+}
+
+bool pmHDUReadWeight(pmHDU *hdu, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return hduRead(hdu, &hdu->weights, fits);
+}
+
+// Write an HDU to a FITS file
+static bool hduWrite(pmHDU *hdu,        // HDU to write
+                     psArray *images,   // Images to write
+                     psFits *fits       // FITS file to which to write
+                    )
+{
+    assert(hdu);
+    assert(fits);
+
+    psTrace("psModules.camera", 7, "Writing HDU %s\n", hdu->extname);
+
+    if (!images && !hdu->header) {
+        psLogMsg(__func__, PS_LOG_WARN, "Nothing to write for HDU %s\n", hdu->extname);
+        return false;
+    }
+
+    // Preserve the extension name, if it's the PHU
+    char *extname = hdu->extname;       // The name of the extension
+    if (!extname && hdu->header) {
+        bool mdok = true;               // Status of MD lookup
+        extname = psMetadataLookupStr(&mdok, hdu->header, "EXTNAME");
+        if (!mdok || !extname || strlen(extname) == 0) {
+            extname = "";
+        }
+    }
+
+    // Make sure it's recognisable as what it's supposed to be
+    if (!hdu->header) {
+        hdu->header = psMetadataAlloc();
+    }
+    if (!pmConfigConformHeader(hdu->header, hdu->format)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to conform header to format.\n");
+        return false;
+    }
+
+    // Only a header
+    if (!images && !psFitsWriteBlank(fits, hdu->header, extname)) {
+        psError(PS_ERR_IO, false, "Unable to write header for extension %s\n", extname);
+        return false;
+    }
+
+    if (images) {
+        psTrace("psModules.camera", 9, "Writing pixels for %s\n", hdu->extname);
+        if (!psFitsWriteImageCube(fits, hdu->header, images, extname)) {
+            psError(PS_ERR_IO, false, "Unable to write image to extension %s\n", hdu->extname);
+            return false;
+        }
+    }
+    return true;
+}
+
+// XXX: Add a region specifier?
+bool pmHDUWrite(pmHDU *hdu, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return hduWrite(hdu, hdu->images, fits);
+}
+
+bool pmHDUWriteMask(pmHDU *hdu, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return hduWrite(hdu, hdu->masks, fits);
+}
+
+bool pmHDUWriteWeight(pmHDU *hdu, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu, false);
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+
+    return hduWrite(hdu, hdu->weights, fits);
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmHDU.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmHDU.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmHDU.h	(revision 15358)
@@ -0,0 +1,82 @@
+/* @file pmHDU.h
+ * @brief Define a header data unit (from a FITS file), with functions to read and write
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:14 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_HDU_H
+#define PM_HDU_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// An instance of the FITS Header Data Unit
+///
+/// Of course, it is not an exact replica of a FITS HDU --- they have no mask and weight data, but these are
+/// stored here for convenience --- it keeps all the relevant data about the image in one place.
+typedef struct
+{
+    psString extname;                   ///< The extension name
+    bool blankPHU;                      ///< Is this a blank FITS Primary Header Unit, i.e., no data?
+    psMetadata *format;                 ///< The camera format
+    psMetadata *header;                 ///< The FITS header, or NULL if primary for FITS; or section info
+    psArray *images;                    ///< The pixel data
+    psArray *weights;                   ///< The pixel data
+    psArray *masks;                     ///< The pixel data
+}
+pmHDU;
+
+
+/// Allocator for pmHDU
+pmHDU *pmHDUAlloc(const char *extname   ///< Extension name, or NULL for PHU
+                 );
+
+/// Read the HDU header only
+///
+/// Moves to the appropriate extension
+bool pmHDUReadHeader(pmHDU *hdu,        ///< HDU for which to read header
+                     psFits *fits       ///< FITS file from which to read
+                    );
+
+/// Read the HDU header and pixels
+///
+/// Moves to the appropriate extension
+bool pmHDURead(pmHDU *hdu,              ///< HDU to read
+               psFits *fits             ///< FITS file to read from
+              );
+
+/// Read the HDU header and mask
+///
+/// Moves to the appropriate extension
+bool pmHDUReadMask(pmHDU *hdu,          ///< HDU to read
+                   psFits *fits         ///< FITS file to read from
+                  );
+
+/// Read the HDU header and weight map
+///
+/// Moves to the appropriate extension
+bool pmHDUReadWeight(pmHDU *hdu,        ///< HDU to read
+                     psFits *fits       ///< FITS file to read from
+                    );
+
+/// Write the HDU header and pixels
+bool pmHDUWrite(pmHDU *hdu,             ///< HDU to write
+                psFits *fits            ///< FITS file to write to
+               );
+
+/// Write the HDU header and mask
+bool pmHDUWriteMask(pmHDU *hdu,         ///< HDU to write
+                    psFits *fits        ///< FITS file to write to
+                   );
+
+/// Write the HDU header and weight map
+bool pmHDUWriteWeight(pmHDU *hdu,       ///< HDU to write
+                      psFits *fits      ///< FITS file to write to
+                     );
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmHDUGenerate.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmHDUGenerate.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmHDUGenerate.c	(revision 15358)
@@ -0,0 +1,712 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include <strings.h>            /* for strn?casecmp */
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmHDUUtils.h"
+#include "pmConcepts.h"
+#include "pmConceptsStandard.h"
+#include "pmHDUGenerate.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Add cells in a chip to a list
+static bool addCellsFromChip(psList *list, // List of cells
+                             const pmChip *chip // The chip from which to add cells
+                            )
+{
+    assert(list);
+    assert(chip);
+
+    psArray *cells = chip->cells;       // Array of cells
+    bool result = true;                 // Result of adding cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // A cell
+        if (!cell->hdu) {               // Don't add cells that have their own HDU
+            result |= psListAdd(list, PS_LIST_TAIL, cell);
+        }
+    }
+
+    return result;
+}
+
+// Add cells in an FPA to a list
+static bool addCellsFromFPA(psList *list, // List of cells
+                            const pmFPA *fpa // The FPA from which to add cells
+                           )
+{
+    assert(list);
+    assert(fpa);
+
+    psArray *chips = fpa->chips;        // Array of chips
+    bool result = true;                 // Result of adding cells
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // A chip
+        if (! chip->hdu) {              // Don't add chips that have their own HDU
+            result |= addCellsFromChip(list, chip);
+        }
+    }
+
+    return result;
+}
+
+// Get the maximum extent of the HDU from the trimsec and biassecs
+static bool sizeHDU(int *xSize, int *ySize, // Size of HDU
+                    psList *cells       // List of cells
+                   )
+{
+    psListIterator *cellsIter = psListIteratorAlloc(cells, PS_LIST_HEAD, false); // Iterator for cells
+    pmCell *cell = NULL;                // The cell from iteration
+    bool mdok = true;                   // Status of MD lookup
+    *xSize = 0;
+    *ySize = 0;
+    while ((cell = psListGetAndIncrement(cellsIter))) {
+        psRegion *trimsec = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.TRIMSEC"); // Trim section
+        if (mdok && trimsec && !psRegionIsNaN(*trimsec)) {
+            *xSize = PS_MAX(trimsec->x1, *xSize);
+            *ySize = PS_MAX(trimsec->y1, *ySize);
+        } else {
+            psFree(cellsIter);
+            return false;
+        }
+        psList *biassecs = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.BIASSEC"); // Bias sections
+        if (mdok && biassecs) {
+            psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false); // Iterator
+            psRegion *biassec = NULL;   // The bias section
+            while ((biassec = psListGetAndIncrement(biassecsIter))) {
+                if (!psRegionIsNaN(*biassec)) {
+                    *xSize = PS_MAX(biassec->x1, *xSize);
+                    *ySize = PS_MAX(biassec->y1, *ySize);
+                } else {
+                    psFree(biassecsIter);
+                    psFree(cellsIter);
+                    return false;
+                }
+            }
+            psFree(biassecsIter);
+        }
+    }
+    psFree(cellsIter);
+
+    return (*xSize != 0 && *ySize != 0);
+}
+
+
+static psRegion *sectionForImage(int *position, // Position on the output image, updated
+                                 const psImage *image, // Image containing the sizes and offsets
+                                 int readdir // Read direction, 1=rows, 2=cols
+                                )
+{
+    psRegion *region = NULL;            // The region to return
+    switch (readdir) {
+    case 1:                           // Read direction is rows
+        region = psRegionAlloc(*position, *position + image->numCols, image->row0,
+                               image->row0 + image->numRows);
+        *position += image->numCols;
+        break;
+    case 2:                           // Read direction is columns
+        region = psRegionAlloc(image->col0, image->col0 + image->numCols, *position,
+                               *position + image->numRows);
+        *position += image->numRows;
+        break;
+    default:
+        psAbort("Shouldn't ever get here!\n");
+    }
+
+    return region;
+}
+
+static bool doBiasSections(int *position, // Position on the output image, updated
+                           int *readdir,// Read direction for cells
+                           pmCell *cell // Cell
+                          )
+{
+    psMetadataItem *biassecItem = psMetadataLookup(cell->concepts, "CELL.BIASSEC"); // Bias sections
+    if (!biassecItem) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.BIASSEC has not been initialised in cell --- "
+                 "ignored.\n");
+        return false;
+    }
+    psFree(biassecItem->data.V);        // Blow away the old list
+    psList *biassecs = psListAlloc(NULL);
+    biassecItem->data.V = biassecs;
+
+    bool mdok = true;                   // Status of MD lookup
+    int cellreaddir = psMetadataLookupS32(&mdok, cell->concepts, "CELL.READDIR"); // Read direction
+    if (!mdok || (cellreaddir != 1 && cellreaddir != 2)) {
+        // Probably unnecessary, but just in case....
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.READDIR is not set in cell --- ignored.\n");
+        return false;
+    }
+    if (*readdir == 0) {
+        *readdir = cellreaddir;
+    } else if (*readdir != cellreaddir) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.READDIR does not match read direction for HDU --- ignored.\n");
+        return false;
+    }
+
+    pmReadout *readout = cell->readouts->data[0]; // The first readout, as representative
+    psList *biases = readout->bias; // The bias images from the readout
+
+    psListIterator *biasIter = psListIteratorAlloc(biases, PS_LIST_HEAD, true); // Iterator for biases
+    psImage *bias = NULL;       // Bias image from iteration
+    while ((bias = psListGetAndIncrement(biasIter))) {
+        // Construct a region
+        psRegion *biassec = sectionForImage(position, bias, *readdir);
+        psListAdd(biassecs, PS_LIST_TAIL, biassec);
+        psFree(biassec);        // Drop reference
+    }
+    psFree(biasIter);
+
+    return true;
+}
+
+// Attempt to read CELL.TRIMSEC and CELL.BIASSEC from the cell format if they are specified by VALUE
+static bool readTrimBias(psList *cells // List of cells below the HDU
+    )
+{
+    bool mdok;                          // Status of MD lookup
+    psListIterator *cellsIter = psListIteratorAlloc(cells, PS_LIST_HEAD, false); // Iterator for cells
+    pmCell *cell = NULL;                // Cell from iteration
+    bool allFixed = true;               // We're able to fix all TRIMSEC and BIASSEC
+    while ((cell = psListGetAndIncrement(cellsIter))) {
+        psMetadataItem *trimsecItem = psMetadataLookup(cell->concepts, "CELL.TRIMSEC"); // Item with trimsec
+        if (!trimsecItem || trimsecItem->type != PS_DATA_REGION) {
+            psWarning("CELL.TRIMSEC has not been initialised in cell --- ignored.\n");
+            return false;
+        }
+        psRegion *trimsec = trimsecItem->data.V; // Trim section
+        if (!trimsec || psRegionIsNaN(*trimsec)) {
+            const char *trimsecSource = psMetadataLookupStr(&mdok, cell->config, "CELL.TRIMSEC.SOURCE");
+            if (strcmp(trimsecSource, "VALUE") == 0) {
+
+                const char *trimsecStr = psMetadataLookupStr(&mdok, cell->config, "CELL.TRIMSEC");
+                if (!trimsec) {
+                    trimsec = trimsecItem->data.V = psRegionAlloc(NAN, NAN, NAN, NAN);
+                }
+                *trimsec = psRegionFromString(trimsecStr);
+            } else {
+                allFixed = false;
+            }
+        }
+
+        psMetadataItem *biassecItem = psMetadataLookup(cell->concepts, "CELL.BIASSEC"); // Bias sections
+        if (!biassecItem) {
+            psWarning("CELL.BIASSEC has not been initialised in cell --- ignored.\n");
+            return false;
+        }
+        psList *biassecs = biassecItem->data.V;
+        if (!biassecs || biassecs->n != 0) {
+            allFixed = false;
+            continue;
+        }
+
+        const char *biassecSource = psMetadataLookupStr(&mdok, cell->config, "CELL.BIASSEC.SOURCE");
+        if (strcmp(biassecSource, "VALUE") == 0) {
+            const char *biassecStr = psMetadataLookupStr(&mdok, cell->config, "CELL.BIASSEC");
+            psFree(biassecItem->data.V);
+            biassecItem->data.V = p_pmConceptParseRegions(biassecStr);
+        } else {
+            allFixed = false;
+        }
+    }
+    psFree(cellsIter);
+
+    return allFixed;
+}
+
+
+// Generate CELL.TRIMSEC and CELL.BIASSEC for the cells
+static bool generateTrimBias(psList *cells // List of cells below the HDU
+                            )
+{
+    pmCell *cell = NULL;                // Cell from iteration
+    int numCells = cells->n;            // Number of cells
+    int cellNum = 0;                    // The cell number
+    int position = 0;                   // Position on the image
+    bool mdok = true;                   // Status of MD lookup
+    int readdir = 0;                    // Read direction (1=rows, 2=cols)
+
+    // First run through to do the LHS biases
+    psListIterator *cellsIter = psListIteratorAlloc(cells, PS_LIST_HEAD, false); // Iterator for cells
+    bool done = false;                  // Done with iteration (due to being halfway through)?
+    while ((cell = psListGetAndIncrement(cellsIter)) && !done) {
+        if (cellNum <= numCells/2 - 1) {
+            doBiasSections(&position, &readdir, cell);
+            cellNum++;
+        } else {
+            done = true;
+        }
+    }
+
+    // Second run through to do the trim sections
+    psListIteratorSet(cellsIter, PS_LIST_HEAD);
+    while ((cell = psListGetAndIncrement(cellsIter))) {
+        psMetadataItem *trimsecItem = psMetadataLookup(cell->concepts, "CELL.TRIMSEC"); // Item with trimsec
+        if (!trimsecItem || trimsecItem->type != PS_DATA_REGION) {
+            psLogMsg(__func__, PS_LOG_WARN, "CELL.TRIMSEC has not been initialised in cell --- "
+                     "ignored.\n");
+            continue;
+        }
+        psRegion *trimsec = trimsecItem->data.V; // Trim section
+
+        int cellreaddir = psMetadataLookupS32(&mdok, cell->concepts, "CELL.READDIR"); // Read direction
+        if (!mdok || (cellreaddir != 1 && cellreaddir != 2)) {
+            // Probably unnecessary, but just in case....
+            psLogMsg(__func__, PS_LOG_WARN, "CELL.READDIR is not set in cell --- ignored.\n");
+            continue;
+        }
+        if (readdir == 0 && mdok && cellreaddir != 0) {
+            readdir = cellreaddir;
+        } else if (readdir != cellreaddir) {
+            psLogMsg(__func__, PS_LOG_WARN, "CELL.READDIR for cells within the HDU do not match!\n");
+            cellreaddir = readdir;
+        }
+
+        pmReadout *readout = cell->readouts->data[0]; // The first readout, as representative
+        // The proper image, used to get the size
+        psImage *image = readout->image ? readout->image : (readout->mask ? readout->mask : readout->weight);
+        if (!image) {
+            continue;
+        }
+        if (readout->mask &&
+                (readout->mask->numCols != image->numCols || readout->mask->numRows != image->numRows)) {
+            psLogMsg(__func__, PS_LOG_WARN, "Image and mask have different sizes (%dx%d vs %dx%d)!\n",
+                     image->numCols, image->numRows, readout->mask->numCols, readout->mask->numRows);
+        }
+        if (readout->weight &&
+                (readout->weight->numCols != image->numCols || readout->weight->numRows != image->numRows)) {
+            psLogMsg(__func__, PS_LOG_WARN, "Image and weight have different sizes (%dx%d vs %dx%d)!\n",
+                     image->numCols, image->numRows, readout->weight->numCols, readout->weight->numRows);
+        }
+        // New reference
+        trimsec = sectionForImage(&position, image, cellreaddir);
+        psFree(trimsecItem->data.V);
+        trimsecItem->data.V = trimsec;
+    }
+
+    // A final run through to do the RHS biases
+    psListIteratorSet(cellsIter, cellNum);
+    while ((cell = psListGetAndIncrement(cellsIter))) {
+        doBiasSections(&position, &readdir, cell);
+    }
+
+    // Clean up
+    psFree(cellsIter);
+
+    return (position > 0);
+}
+
+// Check the type for a current image against a previous type
+static psElemType checkTypes(psElemType previous, // Previously defined type, or 0
+                             psElemType current // Current type
+                            )
+{
+    if (previous == 0) {
+        return current;
+    }
+
+    if (previous != current) {
+        psLogMsg(__func__, PS_LOG_WARN, "Images within the HDU are of different types "
+                 "(%x vs %x) --- promoting\n", previous, current);
+        return PS_MAX(previous, current);
+    }
+
+    return previous;
+}
+
+
+// Paste the source image into the target, according to the provided region.  The source is then updated to
+// reference the region within the target.
+static psImage *pasteImage(psImage *target, // Target image, into which the paste is made
+                           psImage *source,// Source image, from which the paste is made, and then changed
+                           psRegion *region // Image section into which to paste
+                          )
+{
+    if (source->numCols != region->x1 - region->x0 || source->numRows != region->y1 - region->y0) {
+        psString regionString = psRegionToString(*region);
+        psLogMsg(__func__, PS_LOG_WARN, "Image size (%dx%d) does not match region (%s).\n",
+                 source->numCols, source->numRows, regionString);
+        psFree(regionString);
+    }
+    psImageOverlaySection(target, source, region->x0, region->y0, "=");
+
+    // Reference the HDU version, so that subsequent changes will touch the HDU
+    return psImageSubset(target, *region);
+}
+
+
+// Generate the HDU, given a list of cells below that HDU.  This is the main engine function, that does all
+// the work.
+static bool generateHDU(pmHDU *hdu,     // HDU to generate
+                        psList *cells   // List of cells
+                       )
+{
+    // Check the number of readouts is consistent within the HDU
+    int numReadouts = -1;               // Number of readouts
+    psElemType imageType = 0;           // Type of readout images
+    psElemType maskType = 0;            // Type of readout masks
+    psElemType weightType = 0;          // Type of readout weights
+    {
+        psListIterator *iter = psListIteratorAlloc(cells, PS_LIST_HEAD, false); // Iterator for cells
+        pmCell *cell = NULL;                // The cell from iteration
+        while ((cell = psListGetAndIncrement(iter)))
+        {
+            psArray *readouts = cell->readouts;
+            if (numReadouts == -1) {
+                numReadouts = readouts->n;
+            } else if (readouts->n != numReadouts) {
+                psError(PS_ERR_IO, true, "Number of readouts doesn't match: %ld vs %d\n", readouts->n,
+                        numReadouts);
+                return false;
+            }
+            for (int i = 0; i < numReadouts; i++) {
+                pmReadout *readout = readouts->data[i]; // The readout
+                if (!readout) {
+                    continue;
+                }
+
+                if (!hdu->images && readout->image) {
+                    imageType = checkTypes(imageType, readout->image->type.type);
+                }
+                if (!hdu->masks && readout->mask) {
+                    maskType = checkTypes(maskType, readout->mask->type.type);
+                }
+                if (!hdu->weights && readout->weight) {
+                    weightType = checkTypes(weightType, readout->weight->type.type);
+                }
+            }
+        }
+        psFree(iter);
+    }
+    if (numReadouts == 0 || (imageType == 0 && maskType == 0 && weightType == 0)) {
+        // Nothing from which to create an HDU
+        psFree(cells);
+        return false;
+    }
+
+    // Get the size of the HDU, either from existing trimsec and biassec, or generate these and try again
+    int xSize = 0, ySize = 0;           // Size of HDU
+    if (!sizeHDU(&xSize, &ySize, cells) &&
+        !(readTrimBias(cells) && sizeHDU(&xSize, &ySize, cells)) &&
+        !(generateTrimBias(cells) && sizeHDU(&xSize, &ySize, cells))) {
+        psError(PS_ERR_IO, true, "Unable to determine size of HDU!\n");
+        return false;
+    }
+
+    // Generate the HDU
+    if (imageType) {
+        hdu->images = psArrayAlloc(numReadouts);
+        for (int i = 0; i < numReadouts; i++) {
+            psImage *image = psImageAlloc(xSize, ySize, imageType);
+            psImageInit(image, 0.0);
+            hdu->images->data[i] = image;
+        }
+    }
+    if (maskType) {
+        hdu->masks = psArrayAlloc(numReadouts);
+        for (int i = 0; i < numReadouts; i++) {
+            psImage *mask = psImageAlloc(xSize, ySize, maskType);
+            psImageInit(mask, 0);
+            hdu->masks->data[i] = mask;
+        }
+    }
+    if (weightType) {
+        hdu->weights = psArrayAlloc(numReadouts);
+        for (int i = 0; i < numReadouts; i++) {
+            psImage *weight = psImageAlloc(xSize, ySize, weightType);
+            psImageInit(weight, 0.0);
+            hdu->weights->data[i] = weight;
+        }
+    }
+
+    // Insert the pixels into the HDU
+    {
+        psListIterator *iter = psListIteratorAlloc(cells, PS_LIST_HEAD, false); // Iterator for cells
+        pmCell *cell = NULL;           // The cell from iteration
+        bool mdok = true;               // Result of MD lookup
+        while ((cell = psListGetAndIncrement(iter)))
+        {
+            psRegion *trimsec = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.TRIMSEC"); // Trim section
+            if (!mdok || !trimsec) {
+                psAbort("Shouldn't ever get here --- CELL.TRIMSEC should have been set above.\n");
+            }
+            psList *biassecs = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.BIASSEC"); // Bias secionts
+            if (!mdok || !biassecs) {
+                psAbort("Shouldn't ever get here --- CELL.BIASSEC should have been set above.\n");
+            }
+            psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false); // Iterator
+
+            psArray *readouts = cell->readouts; // Array of readouts
+            psArray *hduImages = hdu->images; // Array of images in the HDU
+            psArray *hduMasks = hdu->masks; // Array of masks in the HDU
+            psArray *hduWeights = hdu->weights; // Array of weights in the HDU
+            for (int i = 0; i < readouts->n; i++) {
+                pmReadout *readout = readouts->data[i]; // The readout of interest
+                if (!readout) {
+                    continue;
+                }
+
+                if (readout->image) {
+                    psImage *new = pasteImage(hduImages->data[i], readout->image, trimsec);
+                    psFree(readout->image);
+                    readout->image = new;
+                }
+                if (readout->mask) {
+                    psImage *new = pasteImage(hduMasks->data[i], readout->mask, trimsec);
+                    psFree(readout->mask);
+                    readout->mask = new;
+                }
+                if (readout->weight) {
+                    psImage *new = pasteImage(hduWeights->data[i], readout->weight, trimsec);
+                    psFree(readout->weight);
+                    readout->weight = new;
+                }
+
+                if (biassecs->n != readout->bias->n) {
+                    psLogMsg(__func__, PS_LOG_WARN, "Number of bias sections (%ld) and number of biases (%ld)"
+                             " do not match.\n", biassecs->n, readout->bias->n);
+                }
+                psListIterator *biasIter = psListIteratorAlloc(readout->bias, PS_LIST_HEAD, false); // Iteratr
+                psImage *bias = NULL;   // Bias image, from iteration
+                psListIteratorSet(biassecsIter, PS_LIST_HEAD);
+                psRegion *biassec = NULL; // Bias region, from iteration
+                psList *newBias = psListAlloc(NULL); // New list of bias images
+                while ((bias = psListGetAndIncrement(biasIter)) &&
+                        (biassec = psListGetAndIncrement(biassecsIter))) {
+                    psImage *new = pasteImage(hduImages->data[i], bias, biassec);
+                    psListAdd(newBias, PS_LIST_TAIL, new);
+                    psFree(new);        // Drop reference
+                }
+                psFree(biasIter);
+
+                // Add on the new list of bias images
+                psFree(readout->bias);
+                readout->bias = newBias;
+            }
+            psFree(biassecsIter);
+        } // Iterating over cells within the HDU
+        psFree(iter);
+    }
+    psFree(cells);
+
+    return true;
+}
+
+// Return the level that an extension applies to
+static pmFPALevel extensionLevel(pmHDU *hdu // HDU to check
+                                )
+{
+    if (!hdu->format) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "HDU does not have a camera format.\n");
+        return PM_FPA_LEVEL_NONE;
+    }
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *file = psMetadataLookupMetadata(&mdok, hdu->format, "FILE"); // File information for camera format
+    if (!mdok || !file) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Can't file FILE information for camera format "
+                "configuration.\n");
+        return PM_FPA_LEVEL_NONE;
+    }
+    psString extensions = psMetadataLookupStr(&mdok, file, "EXTENSIONS"); // Where the HDUs are
+    if (!mdok || !extensions || strlen(extensions) == 0) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Can't find EXTENSIONS in the FILE information of the camera "
+                "format configuration.\n");
+        return PM_FPA_LEVEL_NONE;
+    }
+    if (strcasecmp(extensions, "CELL") == 0) {
+        return PM_FPA_LEVEL_CELL;
+    }
+    if (strcasecmp(extensions, "CHIP") == 0) {
+        return PM_FPA_LEVEL_CHIP;
+    }
+    if (strcasecmp(extensions, "FPA") == 0) {
+        return PM_FPA_LEVEL_FPA;
+    }
+    if (strcasecmp(extensions, "NONE") == 0) {
+        return PM_FPA_LEVEL_NONE;
+    }
+    // No idea what it is
+    psError(PS_ERR_IO, true, "EXTENSIONS (%s) in FILE information is not FPA, CHIP, CELL or NONE.\n",
+            extensions);
+    return PM_FPA_LEVEL_NONE;
+}
+
+// Generate HDU for cells belonging to a chip --- just an iterator
+static bool generateForCells(pmChip *chip // Chip for which to generate HDUs
+                            )
+{
+    psArray *cells = chip->cells;       // Array of cells
+    bool status = true;                 // Status of HDU generation
+    for (long i = 0; i < cells->n; i++) {
+        status |= pmHDUGenerateForCell(cells->data[i]);
+    }
+    return status;
+}
+
+// Generate HDU for chips belonging to an FPA --- just an iterator
+static bool generateForChips(pmFPA *fpa // FPA for which to generate HDUs
+                            )
+{
+    psArray *chips = fpa->chips;        // Array of chips
+    bool status = true;                 // Status of HDU generation
+    for (long i = 0; i < chips->n; i++) {
+        status |= pmHDUGenerateForChip(chips->data[i]);
+    }
+    return status;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmHDUGenerateForCell(pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+
+    // Get the HDU and a list of cells below it
+    pmHDU *hdu = pmHDUFromCell(cell); // The HDU in the cell
+    if (!hdu) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Can't find an HDU for cell.\n");
+        return false;
+    }
+    if (hdu->images && hdu->masks && hdu->weights) {
+        // It's already here!
+        return true;
+    }
+
+    pmFPALevel extLevel = extensionLevel(hdu);
+    switch (extLevel) {
+    case PM_FPA_LEVEL_NONE:
+    case PM_FPA_LEVEL_CELL: {
+            psList *cells = psListAlloc(NULL); // List of cells below the HDU
+
+            if (cell->hdu) {
+                psListAdd(cells, PS_LIST_TAIL, cell);
+            } else {
+                pmChip *chip = cell->parent;    // The parent chip
+                if (chip->hdu) {
+                    addCellsFromChip(cells, chip);
+                } else {
+                    pmFPA *fpa = chip->parent;  // The parent FPA
+                    if (fpa->hdu) {
+                        addCellsFromFPA(cells, fpa);
+                    }
+                }
+            }
+            if (cells->n == 0) {
+                // Nothing to do
+                return true;
+            }
+
+            return generateHDU(hdu, cells);
+        }
+    case PM_FPA_LEVEL_CHIP:
+    case PM_FPA_LEVEL_FPA:
+        return pmHDUGenerateForChip(cell->parent);
+    default:
+        psAbort("Shouldn't ever get here: check your camera format configuration.\n");
+    }
+    return false;
+}
+
+bool pmHDUGenerateForChip(pmChip *chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+
+    // Get the HDU and a list of cells below it
+    pmHDU *hdu = pmHDUFromChip(chip);   // The HDU in the chip
+    if (!hdu) {
+        // Nothing here; need to look further down
+        return generateForCells(chip);
+    }
+    if (hdu->images && hdu->masks && hdu->weights) {
+        // It's already here!
+        return true;
+    }
+
+    pmFPALevel extLevel = extensionLevel(hdu);
+    switch (extLevel) {
+    case PM_FPA_LEVEL_CELL:
+        // Work on lower levels
+        return generateForCells(chip);
+    case PM_FPA_LEVEL_NONE:
+    case PM_FPA_LEVEL_CHIP: {
+            // Work on this level
+            psList *cells = psListAlloc(NULL);  // List of cells below the HDU
+            if (chip->hdu) {
+                addCellsFromChip(cells, chip);
+            } else {
+                pmFPA *fpa = chip->parent;  // The parent FPA
+                if (fpa->hdu) {
+                    addCellsFromFPA(cells, fpa);
+                }
+            }
+            if (cells->n == 0) {
+                // Nothing to do
+                return true;
+            }
+
+            return generateHDU(hdu, cells);
+        }
+    case PM_FPA_LEVEL_FPA:
+        // Work on higher levels
+        return pmHDUGenerateForFPA(chip->parent);
+    default:
+        psAbort("Shouldn't ever get here: check your camera format configuration.\n");
+    }
+    return false;
+}
+
+
+bool pmHDUGenerateForFPA(pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    // Get the HDU and a list of cells below it
+    pmHDU *hdu = pmHDUFromFPA(fpa);     // The HDU in the FPA
+    if (!hdu) {
+        // Nothing here; need to look further down
+        return generateForChips(fpa);
+    }
+    if (hdu->images && hdu->masks && hdu->weights) {
+        // It's already here!
+        return true;
+    }
+
+    pmFPALevel extLevel = extensionLevel(hdu);
+    switch (extLevel) {
+    case PM_FPA_LEVEL_CELL:
+    case PM_FPA_LEVEL_CHIP:
+        // Work on lower levels
+        return generateForChips(fpa);
+    case PM_FPA_LEVEL_NONE:
+    case PM_FPA_LEVEL_FPA: {
+            // Work on this level
+            psList *cells = psListAlloc(NULL); // List of cells below the HDU
+            if (fpa->hdu) {
+                addCellsFromFPA(cells, fpa);
+            }
+            if (cells->n == 0) {
+                // Nothing to do
+                return true;
+            }
+
+            return generateHDU(hdu, cells);
+        }
+    default:
+        psAbort("Shouldn't ever get here: check your camera format configuration.\n");
+    }
+    return false;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmHDUGenerate.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmHDUGenerate.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmHDUGenerate.h	(revision 15358)
@@ -0,0 +1,53 @@
+/* @file pmHDUGenerate.h
+ * @brief Generate HDU pixels from FPA components that have pixels
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_HDU_GENERATE_H
+#define PM_HDU_GENERATE_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Generate an HDU (with CELL.TRIMSEC, CELL.BIASSEC and pixels) for a cell with pixels
+///
+/// The write functions for the FPA hierarchy use pmHDUWrite, which assumes that the images in the readouts
+/// are subimages of the pixels in the HDU structure.  If this is not the case, the HDU pixels can be
+/// generated using some simple assumptions.  Splices the images and overscans together without regard for
+/// CELL.X0 and CELL.Y0 (for a proper mosaic, see pmFPAMosaic), though it should respect CELL.READDIR (so that
+/// the bias and trim sections match properly).  A warning may be generated after running this function if the
+/// bias and trim sections are specified in the camera format by default values rather than in the header.
+/// Failure of this function is often due to a bad camera format file.
+bool pmHDUGenerateForCell(pmCell *cell  ///< The cell for which to generate an HDU
+                         );
+
+/// Generate an HDU (with CELL.TRIMSEC, CELL.BIASSEC and pixels) for a cell with pixels
+///
+/// The write functions for the FPA hierarchy use pmHDUWrite, which assumes that the images in the readouts
+/// are subimages of the pixels in the HDU structure.  If this is not the case, the HDU pixels can be
+/// generated using some simple assumptions.  Splices the images and overscans together without regard for
+/// CELL.X0 and CELL.Y0 (for a proper mosaic, see pmFPAMosaic), though it should respect CELL.READDIR (so that
+/// the bias and trim sections match properly).  A warning may be generated after running this function if the
+/// bias and trim sections are specified in the camera format by default values rather than in the header.
+/// Failure of this function is often due to a bad camera format file.
+bool pmHDUGenerateForChip(pmChip *chip  ///< The chip for which to generate an HDU
+                         );
+
+// Generate an HDU (with CELL.TRIMSEC, CELL.BIASSEC and pixels) from an FPA with pixels
+///
+/// The write functions for the FPA hierarchy use pmHDUWrite, which assumes that the images in the readouts
+/// are subimages of the pixels in the HDU structure.  If this is not the case, the HDU pixels can be
+/// generated using some simple assumptions.  Splices the images and overscans together without regard for
+/// CELL.X0 and CELL.Y0 (for a proper mosaic, see pmFPAMosaic), though it should respect CELL.READDIR (so that
+/// the bias and trim sections match properly).  A warning may be generated after running this function if the
+/// bias and trim sections are specified in the camera format by default values rather than in the header.
+/// Failure of this function is often due to a bad camera format file.
+bool pmHDUGenerateForFPA(pmFPA *fpa     ///< The fpa for which to generate an HDU
+                        );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmHDUUtils.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmHDUUtils.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmHDUUtils.c	(revision 15358)
@@ -0,0 +1,151 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+
+pmHDU *pmHDUFromFPA(const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    return fpa->hdu;
+}
+
+pmHDU *pmHDUFromChip(const pmChip *chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    pmHDU *hdu = chip->hdu;             // The HDU information
+    if (!hdu) {
+        hdu = pmHDUFromFPA(chip->parent); // Grab HDU info from the FPA
+    }
+
+    return hdu;
+}
+
+pmHDU *pmHDUFromCell(const pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+
+    pmHDU *hdu = cell->hdu;             // The HDU information
+    if (!hdu) {
+        hdu = pmHDUFromChip(cell->parent); // Grab HDU info from the chip
+    }
+
+    return hdu;
+}
+
+pmHDU *pmHDUFromReadout(const pmReadout *readout)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, NULL);
+
+    pmCell *cell = readout->parent; // cell containing this readout;
+    pmHDU *hdu = pmHDUFromCell(cell);
+    return hdu;
+}
+
+// Get the lowest HDU
+pmHDU *pmHDUGetLowest(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+{
+    pmHDU *hdu = NULL;          // The HDU that's at the lowest level
+    if (cell) {
+        hdu = pmHDUFromCell(cell);
+    } else if (chip) {
+        hdu = pmHDUFromChip(chip);
+    } else if (fpa) {
+        hdu = pmHDUFromFPA(fpa);
+    }
+
+    return hdu;
+}
+
+// Get the highest HDU
+pmHDU *pmHDUGetHighest(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+{
+    pmHDU *hdu = NULL;          // The HDU that's at the highest level
+    if (fpa) {
+        hdu = pmHDUFromFPA(fpa);
+    }
+    if (!hdu && chip) {
+        hdu = pmHDUFromChip(chip);
+    }
+    if (!hdu && cell) {
+        hdu = pmHDUFromCell(cell);
+    }
+
+    return hdu;
+}
+
+// Print spaces to indent
+#define INDENT(FILE, LEVEL) \
+{ \
+    for (int i = 0; i < (LEVEL); i++) { \
+        fprintf(FILE, " "); \
+    } \
+}
+
+void pmHDUPrint(FILE *fd, const pmHDU *hdu, int level, bool header)
+{
+    PS_ASSERT_PTR_NON_NULL(hdu,);
+
+    INDENT(fd, level);
+    if (hdu->blankPHU) {
+        fprintf(fd, "HDU: (PHU)\n");
+    } else {
+        fprintf(fd, "HDU: %s\n", hdu->extname);
+    }
+
+    INDENT(fd, level + 1);
+    fprintf(fd, "Format: %p\n", hdu->format);
+    if (header) {
+        INDENT(fd, level + 1);
+        if (hdu->header) {
+            fprintf(fd, "Header:\n");
+            psMetadataPrint(fd, hdu->header, level + 2);
+        } else {
+            fprintf(fd, "No header.\n");
+        }
+    }
+
+    INDENT(fd, level + 1);
+    if (hdu->images) {
+        fprintf(fd, "Images:\n");
+        for (long i = 0; i < hdu->images->n; i++) {
+            psImage *image = hdu->images->data[i]; // Image of interest
+            INDENT(fd, level + 2);
+            fprintf(fd, "%ld: %dx%d\n", i, image->numCols, image->numRows);
+        }
+    } else {
+        fprintf(fd, "NO images.\n");
+    }
+
+    INDENT(fd, level + 1);
+    if (hdu->masks) {
+        fprintf(fd, "Masks:\n");
+        for (long i = 0; i < hdu->masks->n; i++) {
+            psImage *mask = hdu->masks->data[i]; // Mask of interest
+            INDENT(fd, level + 2);
+            fprintf(fd, "%ld: %dx%d\n", i, mask->numCols, mask->numRows);
+        }
+    } else {
+        fprintf(fd, "NO masks.\n");
+    }
+
+    INDENT(fd, level + 1);
+    if (hdu->weights) {
+        fprintf(fd, "Weights:\n");
+        for (long i = 0; i < hdu->weights->n; i++) {
+            psImage *weight = hdu->weights->data[i]; // Weight image of interest
+            INDENT(fd, level + 2);
+            fprintf(fd, "%ld: %dx%d\n", i, weight->numCols, weight->numRows);
+        }
+    } else {
+        fprintf(fd, "NO weights.\n");
+    }
+
+    return;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmHDUUtils.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmHDUUtils.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmHDUUtils.h	(revision 15358)
@@ -0,0 +1,58 @@
+/* @file pmHDUUtils.h
+ * @brief Utility functions for working with an HDU
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_HDU_UTILS_H
+#define PM_HDU_UTILS_H
+
+/// @addtogroup Camera Camera Layout
+/// @{
+
+/// Get the lowest HDU in the hierarchy
+///
+/// The lowest HDU in the hierarchy will be the one with the actual pixels (if all levels are supplied).
+pmHDU *pmHDUGetLowest(const pmFPA *fpa, ///< The FPA
+                      const pmChip *chip, ///< The chip, or NULL
+                      const pmCell *cell ///< The cell, or NULL
+                     );
+
+/// Get the highest HDU in the hierarchy
+///
+/// The highest HDU in the hierarchy will be the PHU (might get NULL if not all levels are supplied)
+pmHDU *pmHDUGetHighest(const pmFPA *fpa, ///< The FPA
+                       const pmChip *chip, ///< The chip, or NULL
+                       const pmCell *cell ///< The cell, or NULL
+                      );
+
+/// Given an FPA, return the HDU (or NULL if all HDUs reside below the FPA)
+pmHDU *pmHDUFromFPA(const pmFPA *fpa    ///< FPA for which to find HDU
+                   );
+
+/// Given a chip, return the HDU (or NULL if it resides below the chip)
+pmHDU *pmHDUFromChip(const pmChip *chip ///< Chip for which to find HDU
+                    );
+
+/// Given a cell, return the HDU
+pmHDU *pmHDUFromCell(const pmCell *cell ///< Cell for which to find HDU
+                    );
+
+/// Given a readout, return the HDU
+pmHDU *pmHDUFromReadout(const pmReadout *readout ///< Readout for which to find HDU
+                       );
+
+/// Print details about an HDU
+///
+/// This is intended for testing or development use.
+void pmHDUPrint(FILE *fd,               ///< File descriptor to which to print
+                const pmHDU *hdu,       ///< HDU to print
+                int level,              ///< Level at which to print
+                bool header             ///< Print header?
+               );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/camera/pmReadoutStack.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmReadoutStack.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmReadoutStack.c	(revision 15358)
@@ -0,0 +1,124 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+
+#include "pmReadoutStack.h"
+
+
+bool pmReadoutUpdateSize(pmReadout *readout, int minCols, int minRows, int numCols, int numRows, bool mask)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    if (readout->image) {
+        *(psS32*) &(readout->col0) = PS_MIN(minCols, readout->col0);
+        *(psS32*) &(readout->row0) = PS_MIN(minRows, readout->row0);
+    } else {
+        *(psS32*) &(readout->col0) = minCols;
+        *(psS32*) &(readout->row0) = minRows;
+    }
+
+    // (re-)Allocate the images
+    if (!readout->image) {
+        readout->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    }
+    if (readout->image->numCols < numCols || readout->image->numRows < numRows) {
+        // Generate the new output image by extending the current one, or making a whole new one
+        psImage *newImage = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        psImageInit(newImage, 0.0);
+        psImageOverlaySection(newImage, readout->image, readout->col0, readout->row0, "=");
+        psFree(readout->image);
+        readout->image = newImage;
+    }
+
+    if (mask) {
+        if (!readout->mask) {
+            readout->mask = psImageAlloc(numCols, numRows, PS_TYPE_MASK);
+        }
+        if (readout->mask->numCols < numCols || readout->mask->numRows < numRows) {
+            psImage *newMask = psImageAlloc(numCols, numRows, PS_TYPE_MASK);
+            psImageInit(newMask, 0);
+            psImageOverlaySection(newMask, readout->mask, readout->col0, readout->row0, "=");
+            psFree(readout->mask);
+            readout->mask = newMask;
+        }
+    }
+
+    return true;
+}
+
+bool pmReadoutStackValidate(int *minInputColsPtr, int *maxInputColsPtr, int *minInputRowsPtr,
+                            int *maxInputRowsPtr, int *numColsPtr, int *numRowsPtr, const psArray *inputs)
+{
+    PS_ASSERT_ARRAY_NON_NULL(inputs, false);
+
+    // Step through each readout in the input image list to determine how big of an output image is needed to
+    // combine these input images.
+    int maxInputCols = 0;               // The largest input column value
+    int maxInputRows = 0;               // The largest input row value
+    int minInputCols = INT_MAX;         // The smallest input column value
+    int minInputRows = INT_MAX;         // The smallest input row value
+    int xSize = 0, ySize = 0;           // The size of the output image
+
+    int xMin = INT_MAX;
+    int yMin = INT_MAX;
+    int xMax = 0;
+    int yMax = 0;
+
+    bool valid = false;                 // Do we have a single valid input?
+    for (long i = 0; i < inputs->n; i++) {
+        pmReadout *readout = inputs->data[i]; // Readout of interest
+
+        if (!readout || !readout->image) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Input readout %ld is NULL or has NULL image.\n", i);
+            return false;
+        }
+
+        // use the trimsec to define the max full range of the output pixels
+        pmCell *cell = readout->parent; // The parent cell
+        bool mdok = true;       // Status of MD lookup
+        psRegion *trimsec = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.TRIMSEC"); // Trim section
+        if (!mdok || !trimsec || psRegionIsNaN(*trimsec)) {
+            psWarning("CELL.TRIMSEC is not set for readout %ld --- ignored.\n", i);
+        } else {
+            xSize = PS_MAX(xSize, trimsec->x1 - trimsec->x0);
+            ySize = PS_MAX(ySize, trimsec->y1 - trimsec->y0);
+            xMin  = PS_MIN(xMin,  trimsec->x0);
+            xMax  = PS_MAX(xMax,  trimsec->x1);
+            yMin  = PS_MIN(yMin,  trimsec->y0);
+            yMax  = PS_MAX(yMax,  trimsec->y1);
+        }
+
+        valid = true;
+
+        // Range of pixels on output images
+        minInputCols = PS_MAX(xMin, PS_MIN(minInputCols, readout->col0));
+        maxInputCols = PS_MIN(xMax, PS_MAX(maxInputCols, readout->col0 + readout->image->numCols));
+        minInputRows = PS_MAX(yMin, PS_MIN(minInputRows, readout->row0));
+        maxInputRows = PS_MIN(yMax, PS_MAX(maxInputRows, readout->row0 + readout->image->numRows));
+        psTrace("psModules.camera", 7, "Readout %ld: offset %d,%d; size %dx%d\n", i,
+                readout->col0, readout->row0, readout->image->numCols, readout->image->numRows);
+    }
+
+    if (minInputColsPtr) {
+        *minInputColsPtr = minInputCols;
+    }
+    if (maxInputColsPtr) {
+        *maxInputColsPtr = maxInputCols;
+    }
+    if (minInputRowsPtr) {
+        *minInputRowsPtr = minInputRows;
+    }
+    if (maxInputRowsPtr) {
+        *maxInputRowsPtr = maxInputRows;
+    }
+    if (numColsPtr) {
+        *numColsPtr = xSize;
+    }
+    if (numRowsPtr) {
+        *numRowsPtr = ySize;
+    }
+
+    return valid;
+}
Index: /branches/eam_branch_20071023/psModules/src/camera/pmReadoutStack.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/camera/pmReadoutStack.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/camera/pmReadoutStack.h	(revision 15358)
@@ -0,0 +1,22 @@
+#ifndef PM_READOUT_STACK_H
+#define PM_READOUT_STACK_H
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+/// Update an output readout (for a stack) with the correct col0,row0 and the image size
+bool pmReadoutUpdateSize(pmReadout *readout, ///< Readout which to update
+                         int minCols, int minRows, ///< Minimum coordinates
+                         int numCols, int numRows, ///< Size of images
+                         bool mask      ///< Worry about the mask?
+    );
+
+/// Determine how large an output image is needed to combine the input readouts
+bool pmReadoutStackValidate(int *minInputColsPtr, int *maxInputColsPtr, ///< Min and max size in x
+                            int *minInputRowsPtr, int *maxInputRowsPtr, ///< Min and max size in y
+                            int *numColsPtr, int *numRowsPtr, ///< Size of image
+                            const psArray *inputs ///< Array of pmReadouts
+    );
+
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/concepts/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/.cvsignore	(revision 15358)
@@ -0,0 +1,6 @@
+.deps
+.libs
+Makefile
+Makefile.in
+*.la
+*.lo
Index: /branches/eam_branch_20071023/psModules/src/concepts/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/Makefile.am	(revision 15358)
@@ -0,0 +1,23 @@
+noinst_LTLIBRARIES = libpsmodulesconcepts.la
+
+libpsmodulesconcepts_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS)
+libpsmodulesconcepts_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulesconcepts_la_SOURCES  = \
+	pmConcepts.c \
+	pmConceptsAverage.c \
+	pmConceptsRead.c \
+	pmConceptsWrite.c \
+	pmConceptsStandard.c \
+	pmConceptsPhotcode.c \
+	pmConceptsUpdate.c
+
+pkginclude_HEADERS = \
+	pmConcepts.h \
+	pmConceptsAverage.h \
+	pmConceptsRead.h \
+	pmConceptsWrite.h \
+	pmConceptsStandard.h \
+	pmConceptsPhotcode.h \
+	pmConceptsUpdate.h
+
+CLEANFILES = *~
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConcepts.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConcepts.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConcepts.c	(revision 15358)
@@ -0,0 +1,976 @@
+// XXX *REALLY* need generic "concept update" and "concept read" functions that handles the type transparently
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <pslib.h>
+#include <string.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmHDUUtils.h"
+#include "pmConcepts.h"
+#include "pmConceptsRead.h"
+#include "pmConceptsWrite.h"
+#include "pmConceptsStandard.h"
+#include "pmConceptsUpdate.h"
+
+static bool conceptsInitialised = false;// Have concepts been read?
+static psMetadata *conceptsFPA = NULL;  // Known concepts for FPA
+static psMetadata *conceptsChip = NULL; // Known concepts for chip
+static psMetadata *conceptsCell = NULL; // Known concepts for cell
+
+// Return the appropriate concepts metadata, given the level
+static psMetadata *conceptsFromLevel(pmFPALevel level)
+{
+    switch (level) {
+    case PM_FPA_LEVEL_FPA:
+        return conceptsFPA;
+    case PM_FPA_LEVEL_CHIP:
+        return conceptsChip;
+    case PM_FPA_LEVEL_CELL:
+        return conceptsCell;
+    default:
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Invalid concept level provided: %d\n", level);
+        return NULL;
+    }
+}
+
+// Free a concept
+static void conceptSpecFree(pmConceptSpec *spec)
+{
+    psFree(spec->blank);
+}
+
+pmConceptSpec *pmConceptSpecAlloc(psMetadataItem *blank, pmConceptParseFunc parse,
+                                  pmConceptFormatFunc format, bool required)
+{
+    pmConceptSpec *spec = psAlloc(sizeof(pmConceptSpec));
+    psMemSetDeallocator(spec, (psFreeFunc)conceptSpecFree);
+
+    spec->blank = psMemIncrRefCounter(blank);
+    spec->parse = parse;
+    spec->format = format;
+    spec->required = required;
+
+    return spec;
+}
+
+psList *pmConceptsList(pmFPALevel level)
+{
+    if (!conceptsInitialised) {
+        pmConceptsInit();
+    }
+
+    // Get the appropriate concepts
+    psMetadata *concepts = conceptsFromLevel(level); // Metadata of concepts specs
+    if (!concepts) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Invalid concept level provided: %d\n", level);
+        return NULL;
+    }
+
+    // Pull out the names
+    psList *list = psListAlloc(NULL);   // List of concepts' names
+    psMetadataIterator *iter = psMetadataIteratorAlloc(concepts, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        psListAdd(list, PS_LIST_TAIL, item->name);
+    }
+    psFree(iter);
+    return list;
+}
+
+bool pmConceptGetRequired(const char *name, pmFPALevel level)
+{
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+    if (!conceptsInitialised) {
+        pmConceptsInit();
+    }
+
+    psMetadata *concepts = conceptsFromLevel(level); // The metadata of known concepts
+
+    bool mdok;                          // Status of MD lookup
+    pmConceptSpec *spec = psMetadataLookupPtr(&mdok, concepts, name); // The specification
+    if (!spec) {
+        // Won't throw an error, because we can't distinguish an error from the desired result.
+        // However, that doesn't really matter, because if we can't find it, then it can't be required!
+        return false;
+    }
+
+    return spec->required;
+}
+
+bool pmConceptSetRequired(const char *name, pmFPALevel level, bool required)
+{
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    if (!conceptsInitialised) {
+        pmConceptsInit();
+    }
+
+    psMetadata *concepts = conceptsFromLevel(level); // The metadata of known concepts
+
+    bool mdok;                          // Status of MD lookup
+    pmConceptSpec *spec = psMetadataLookupPtr(&mdok, concepts, name); // The specification
+    if (!spec) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to find defined concept %s in level %d.",
+                name, level);
+        return false;
+    }
+    spec->required = required;
+
+    return true;
+}
+
+bool pmConceptRegister(psMetadataItem *blank, pmConceptParseFunc parse,
+                        pmConceptFormatFunc format, bool required, pmFPALevel level)
+{
+    PS_ASSERT_PTR_NON_NULL(blank, false);
+
+    if (!conceptsInitialised) {
+        pmConceptsInit();
+    }
+
+    psMetadata *target = conceptsFromLevel(level); // The metadata of known concepts to write to
+    if (!target) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, false,
+                "Unable to register concept at invalid concept level.");
+        return false;
+    }
+
+    pmConceptSpec *spec = pmConceptSpecAlloc(blank, parse, format, required); // The concept specification
+    psMetadataAdd(target, PS_LIST_TAIL, blank->name, PS_DATA_UNKNOWN | PS_META_REPLACE,
+                  "Concepts specification", spec);
+    psFree(spec);                       // Drop reference
+
+    return true;
+}
+
+
+// Set all registered concepts to blank value for the specified level
+static bool conceptsBlank(psMetadata **specs, // One of the concepts specifications
+                          psMetadata *target // Place to install the concepts
+                         )
+{
+    assert(specs);
+    assert(target);
+
+    if (!conceptsInitialised) {
+        pmConceptsInit();
+    }
+    psMetadataIterator *specsIter = psMetadataIteratorAlloc(*specs, PS_LIST_HEAD, NULL); // Iterator on specs
+    psMetadataItem *specItem = NULL;    // Item from the specs metadata
+    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+        psTrace("psModules.concepts", 9, "Blanking %s...\n", specItem->name);
+        pmConceptSpec *spec = specItem->data.V; // The specification
+        psMetadataItem *blank = spec->blank; // The concept
+        psMetadataItem *copy = NULL;    // Copy of the blank concept
+        // Trap the lists, which can't be copied in the ordinary way without a warning
+        if (blank->type == PS_DATA_LIST) {
+            copy = psMetadataItemAllocPtr(blank->name, PS_DATA_LIST, blank->comment, blank->data.V);
+        } else {
+            copy = psMetadataItemCopy(blank);
+        }
+        if (!psMetadataAddItem(target, copy, PS_LIST_TAIL, PS_META_REPLACE)) {
+            psLogMsg(__func__, PS_LOG_WARN, "Unable to add blank version of concept %s\n", blank->name);
+        }
+        psFree(copy);                   // Drop reference
+    }
+    psFree(specsIter);
+
+    return true;
+}
+
+
+
+// Read all registered concepts for the specified level
+static bool conceptsRead(psMetadata **specs, // One of the concepts specifications
+                         const pmFPA *fpa,    // The FPA
+                         const pmChip *chip,  // The chip
+                         const pmCell *cell, // The cell
+                         unsigned int *read,     // What's already been read
+                         pmConceptSource source, // The source of the concepts to read
+                         psDB *db,      // Database handle
+                         psMetadata *target // Place into which to read the concepts
+                        )
+{
+    assert(specs);
+    assert(read);
+    assert(target);
+
+    if (!conceptsInitialised) {
+        pmConceptsInit();
+    }
+
+    // At least one HDU is required for the reading functions
+    pmHDU *hduLow = pmHDUGetLowest(fpa, chip, cell); // Lowest HDU.
+    if (!hduLow) {
+        // Can't do anything --- don't record any success, but don't return an error either
+        return true;
+    }
+    pmHDU *hduHigh = pmHDUGetHighest(fpa, chip, cell); // Highest HDU
+
+    bool success = true;                // Success in reading concepts?
+    if (source & PM_CONCEPT_SOURCE_CELLS && !(*read & PM_CONCEPT_SOURCE_CELLS) && cell) {
+        if (p_pmConceptsReadFromCells(target, *specs, cell)) {
+            *read |= PM_CONCEPT_SOURCE_CELLS;
+        } else {
+            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from camera configuration.\n");
+            success = false;
+        }
+    }
+
+    if (source & PM_CONCEPT_SOURCE_DEFAULTS && !(*read & PM_CONCEPT_SOURCE_DEFAULTS)) {
+        if (p_pmConceptsReadFromDefaults(target, *specs, fpa, chip, cell)) {
+            *read |= PM_CONCEPT_SOURCE_DEFAULTS;
+        } else {
+            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from defaults.\n");
+            success = false;
+        }
+    }
+
+    if (source & PM_CONCEPT_SOURCE_PHU && !(*read & PM_CONCEPT_SOURCE_PHU) && hduHigh->header) {
+        if (p_pmConceptsReadFromHeader(target, *specs, fpa, chip, cell)) {
+            *read |= PM_CONCEPT_SOURCE_PHU;
+        } else {
+            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from PHU.\n");
+            success = false;
+        }
+    }
+
+    // If there are multiple HDUs, then it may be that one of them hasn't been read yet (hdu->header not set)
+    if (source & PM_CONCEPT_SOURCE_HEADER && !(*read & PM_CONCEPT_SOURCE_HEADER) &&
+        hduLow != hduHigh && hduLow->header) {
+        if (p_pmConceptsReadFromHeader(target, *specs, fpa, chip, cell)) {
+            *read |= PM_CONCEPT_SOURCE_HEADER;
+        } else {
+            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from header.\n");
+            success = false;
+        }
+    }
+
+    #ifdef HAVE_PSDB
+    if (source & PM_CONCEPT_SOURCE_DATABASE && !(*read & PM_CONCEPT_SOURCE_DATABASE)) {
+        if (p_pmConceptsReadFromDatabase(target, *specs, fpa, chip, cell, db)) {
+            *read |= PM_CONCEPT_SOURCE_DATABASE;
+        } else {
+            psError(PS_ERR_UNKNOWN, false, "Error reading concepts from database.\n");
+            success = false;
+        }
+    }
+    #endif
+
+    pmConceptsUpdate(fpa, chip, cell);
+
+    return success;
+}
+
+// Write all registered concepts for the specified level
+static bool conceptsWrite(psMetadata **specs, // One of the concepts specifications
+                          const pmFPA *fpa,   // The FPA
+                          const pmChip *chip, // The chip
+                          const pmCell *cell, // The cell
+                          pmConceptSource source, // The source of the concepts to write
+                          psDB *db,      // Database handle
+                          const psMetadata *concepts // The concepts to write out
+                         )
+{
+    assert(specs);
+    assert(concepts);
+
+    if (!conceptsInitialised) {
+        pmConceptsInit();
+    }
+
+    psTrace("psModules.concepts", 3, "Writing concepts (%p %p %p): %d\n", fpa, chip, cell, source);
+
+    if (source & PM_CONCEPT_SOURCE_CELLS) {
+        p_pmConceptsWriteToCells(*specs, cell, concepts);
+    }
+    if (source & PM_CONCEPT_SOURCE_DEFAULTS) {
+        p_pmConceptsWriteToDefaults(*specs, fpa, chip, cell, concepts);
+    }
+    if (source & (PM_CONCEPT_SOURCE_PHU | PM_CONCEPT_SOURCE_HEADER)) {
+        p_pmConceptsWriteToHeader(*specs, fpa, chip, cell, concepts);
+    }
+    if (source & PM_CONCEPT_SOURCE_DATABASE) {
+        p_pmConceptsWriteToDatabase(*specs, fpa, chip, cell, db, concepts);
+    }
+
+    return true;
+}
+
+
+bool pmConceptsRead(pmFPA *fpa, pmChip *chip, pmCell *cell, pmConceptSource source, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    bool success = conceptsRead(&conceptsFPA, fpa, chip, cell, &fpa->conceptsRead, source, db, fpa->concepts);
+    if (chip) {
+        success &= conceptsRead(&conceptsChip, fpa, chip, cell, &chip->conceptsRead, source, db, chip->concepts);
+    }
+    if (cell) {
+        success &= conceptsRead(&conceptsCell, fpa, chip, cell, &cell->conceptsRead, source, db, cell->concepts);
+    }
+
+    return success;
+}
+
+
+bool pmConceptsBlankFPA(pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    psTrace("psModules.concepts", 5, "Blanking FPA concepts: %p %p\n", conceptsFPA, fpa->concepts);
+    return conceptsBlank(&conceptsFPA, fpa->concepts);
+}
+
+
+bool pmConceptsReadFPA(pmFPA *fpa, pmConceptSource source, bool propagateDown, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    psTrace("psModules.concepts", 5, "Reading FPA concepts: %p %p\n", conceptsFPA, fpa->concepts);
+    bool success = conceptsRead(&conceptsFPA, fpa, NULL, NULL, &fpa->conceptsRead, source, db, fpa->concepts);
+    if (propagateDown) {
+        psArray *chips = fpa->chips;    // Array of chips
+        for (long i = 0; i < chips->n; i++) {
+            pmChip *chip = chips->data[i]; // Chip of interest
+            if (chip) {
+                success &= pmConceptsReadChip(chip, source, false, true, db);
+            }
+        }
+    }
+
+    return success;
+}
+
+bool pmConceptsWriteFPA(const pmFPA *fpa, pmConceptSource source, bool propagateDown, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    psTrace("psModules.concepts", 5, "Writing FPA concepts: %p %p\n", conceptsFPA, fpa->concepts);
+    bool success = conceptsWrite(&conceptsFPA, fpa, NULL, NULL, source, db, fpa->concepts);
+    if (propagateDown) {
+        psArray *chips = fpa->chips;        // Array of chips
+        for (long i = 0; i < chips->n; i++) {
+            pmChip *chip = chips->data[i];  // Chip of interest
+            if (chip && !chip->hdu) {
+                success &= pmConceptsWriteChip(chip, source, false, true, db);
+            }
+        }
+    }
+    return success;
+}
+
+bool pmConceptsBlankChip(pmChip *chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    psTrace("psModules.concepts", 5, "Blanking chip concepts: %p %p\n", conceptsChip, chip->concepts);
+    return conceptsBlank(&conceptsChip, chip->concepts);
+}
+
+bool pmConceptsReadChip(pmChip *chip, pmConceptSource source, bool propagateUp,
+                        bool propagateDown, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    psTrace("psModules.concepts", 5, "Reading chip concepts: %p %p\n", conceptsChip, chip->concepts);
+    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
+    bool success = conceptsRead(&conceptsChip, fpa, chip, NULL, &chip->conceptsRead, source, db,
+                                chip->concepts);
+    if (propagateUp) {
+        success &= conceptsRead(&conceptsFPA, fpa, chip, NULL, &fpa->conceptsRead, source, db, fpa->concepts);
+    }
+    if (propagateDown) {
+        psArray *cells = chip->cells;        // Array of cells
+        for (long i = 0; i < cells->n; i++) {
+            pmCell *cell = cells->data[i];  // Cell of interest
+            if (cell) {
+                success &= pmConceptsReadCell(cell, source, false, db);
+            }
+        }
+    }
+    return success;
+}
+
+bool pmConceptsWriteChip(const pmChip *chip, pmConceptSource source, bool propagateUp,
+                         bool propagateDown, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    psTrace("psModules.concepts", 5, "Writing chip concepts: %p %p\n", conceptsChip, chip->concepts);
+    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
+    bool success = conceptsWrite(&conceptsChip, fpa, chip, NULL, source, db, chip->concepts);
+    if (propagateUp && !fpa->hdu) {
+        success &= conceptsWrite(&conceptsFPA, fpa, chip, NULL, source, db, fpa->concepts);
+    }
+    if (propagateDown) {
+        psArray *cells = chip->cells;        // Array of cells
+        for (long i = 0; i < cells->n; i++) {
+            pmCell *cell = cells->data[i];  // Cell of interest
+            if (cell && !cell->hdu) {
+                success &= pmConceptsWriteCell(cell, source, false, db);
+            }
+        }
+    }
+    return success;
+}
+
+bool pmConceptsBlankCell(pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    psTrace("psModules.concepts", 5, "Blanking cell concepts: %p %p\n", conceptsCell, cell->concepts);
+    return conceptsBlank(&conceptsCell, cell->concepts);
+}
+
+bool pmConceptsReadCell(pmCell *cell, pmConceptSource source, bool propagateUp, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    psTrace("psModules.concepts", 5, "Reading cell concepts: %p %p\n", conceptsCell, cell->concepts);
+    pmChip *chip = cell->parent;        // Chip to which the cell belongs
+    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
+
+    bool success = conceptsRead(&conceptsCell, fpa, chip, cell, &cell->conceptsRead, source, db,
+                                cell->concepts);
+    if (propagateUp) {
+        success &= conceptsRead(&conceptsChip, fpa, chip, cell, &chip->conceptsRead, source, db,
+                                chip->concepts);
+        success &= conceptsRead(&conceptsFPA, fpa, chip, cell, &fpa->conceptsRead, source, db,
+                                fpa->concepts);
+    }
+
+    return success;
+}
+
+bool pmConceptsWriteCell(const pmCell *cell, pmConceptSource source, bool propagateUp, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    psTrace("psModules.concepts", 5, "Writing cell concepts: %p %p\n", conceptsCell, cell->concepts);
+    pmChip *chip = cell->parent;        // Chip to which the cell belongs
+    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
+
+    bool success = conceptsWrite(&conceptsCell, fpa, chip, cell, source, db, cell->concepts);
+    if (propagateUp) {
+        if (!chip->hdu) {
+            success &= conceptsWrite(&conceptsChip, fpa, chip, cell, source, db, chip->concepts);
+            if (!fpa->hdu) {
+                success &= conceptsWrite(&conceptsFPA, fpa, chip, cell, source, db, fpa->concepts);
+            }
+        }
+    }
+
+    return success;
+}
+
+
+bool pmConceptsInit(void)
+{
+    conceptsInitialised = true;
+
+    p_psMemAllocatePersistent(true);
+
+    bool init = false;                  // Did we initialise anything?
+    if (! conceptsFPA) {
+        conceptsFPA = psMetadataAlloc();
+        init = true;
+
+        // Install the standard concepts
+
+        // FPA.TELESCOPE
+        {
+            psMetadataItem *fpaTelescope = psMetadataItemAllocStr("FPA.TELESCOPE", "Camera used", "");
+            pmConceptRegister(fpaTelescope, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaTelescope);
+        }
+
+        // FPA.INSTRUMENT
+        {
+            psMetadataItem *fpaInstrument = psMetadataItemAllocStr("FPA.INSTRUMENT", "Camera used", "");
+            pmConceptRegister(fpaInstrument, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaInstrument);
+        }
+
+        // FPA.DETECTOR
+        {
+            psMetadataItem *fpaDetector = psMetadataItemAllocStr("FPA.DETECTOR", "Detector used", "");
+            pmConceptRegister(fpaDetector, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaDetector);
+        }
+
+        // FPA.CAMERA
+        // XXX keep or deprecate?
+        {
+            psMetadataItem *fpaCamera = psMetadataItemAllocStr("FPA.CAMERA", "Camera used", "");
+            pmConceptRegister(fpaCamera, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaCamera);
+        }
+
+        // FPA.FOCUS
+        {
+            psMetadataItem *fpaFocus = psMetadataItemAllocF32("FPA.FOCUS", "Telescope focus", NAN);
+            pmConceptRegister(fpaFocus, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaFocus);
+        }
+
+        // FPA.AIRMASS
+        {
+            psMetadataItem *fpaAirmass = psMetadataItemAllocF32("FPA.AIRMASS", "Airmass at boresight", NAN);
+            pmConceptRegister(fpaAirmass, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaAirmass);
+        }
+
+        // FPA.FILTERID
+        {
+            psMetadataItem *fpaFilterid = psMetadataItemAllocStr("FPA.FILTERID",
+                                                                 "Filter used (parsed, abstract name) ", "");
+            pmConceptRegister(fpaFilterid, (pmConceptParseFunc)p_pmConceptParse_FPA_FILTER,
+                              (pmConceptFormatFunc)p_pmConceptFormat_FPA_FILTER, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaFilterid);
+        }
+
+        // FPA.FILTER
+        {
+            psMetadataItem *fpaFilter = psMetadataItemAllocStr("FPA.FILTER", "Filter used", "");
+            pmConceptRegister(fpaFilter, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaFilter);
+        }
+
+        // FPA.POSANGLE
+        {
+            psMetadataItem *fpaPosangle = psMetadataItemAllocF32("FPA.POSANGLE",
+                                          "Position angle of instrument", NAN);
+            pmConceptRegister(fpaPosangle, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaPosangle);
+        }
+
+        // FPA.RADECSYS
+        {
+            psMetadataItem *fpaRadecsys = psMetadataItemAllocStr("FPA.RADECSYS",
+                                          "Celestial coordinate system", "");
+            pmConceptRegister(fpaRadecsys, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaRadecsys);
+        }
+
+        // FPA.RA
+        {
+            psMetadataItem *fpaRa = psMetadataItemAllocF64("FPA.RA", "Right Ascension of boresight", NAN);
+            pmConceptRegister(fpaRa, (pmConceptParseFunc)p_pmConceptParse_FPA_Coords,
+                              (pmConceptFormatFunc)p_pmConceptFormat_FPA_Coords, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaRa);
+        }
+
+        // FPA.DEC
+        {
+            psMetadataItem *fpaDec = psMetadataItemAllocF64("FPA.DEC", "Declination of boresight", NAN);
+            pmConceptRegister(fpaDec, (pmConceptParseFunc)p_pmConceptParse_FPA_Coords,
+                              (pmConceptFormatFunc)p_pmConceptFormat_FPA_Coords, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaDec);
+        }
+
+        // FPA.OBSTYPE
+        {
+            psMetadataItem *fpaObstype = psMetadataItemAllocStr("FPA.OBSTYPE", "Type of observation", "");
+            pmConceptRegister(fpaObstype, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaObstype);
+        }
+
+        // FPA.OBJECT
+        {
+            psMetadataItem *fpaObject = psMetadataItemAllocStr("FPA.OBJECT", "Object of observation", "");
+            pmConceptRegister(fpaObject, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaObject);
+        }
+
+        // FPA.ALT
+        {
+            psMetadataItem *fpaAlt = psMetadataItemAllocF64("FPA.ALT", "Altitude of telescope", NAN);
+            pmConceptRegister(fpaAlt, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaAlt);
+        }
+
+        // FPA.AZ
+        {
+            psMetadataItem *fpaAz = psMetadataItemAllocF64("FPA.AZ", "Azimuth of telescope", NAN);
+            pmConceptRegister(fpaAz, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaAz);
+        }
+
+        // FPA.TIMESYS
+        {
+            psMetadataItem *fpaTimesys = psMetadataItemAllocS32("FPA.TIMESYS", "Time system", -1);
+            pmConceptRegister(fpaTimesys, (pmConceptParseFunc)p_pmConceptParse_TIMESYS,
+                              (pmConceptFormatFunc)p_pmConceptFormat_TIMESYS, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaTimesys);
+        }
+
+        // FPA.TIME
+        {
+            psTime *time = psTimeAlloc(PS_TIME_TAI); // Blank time
+            // Not particularly distinguishing, but should be good enough
+            time->sec = 0;
+            time->nsec = 0;
+            psMetadataItem *fpaTime = psMetadataItemAlloc("FPA.TIME", PS_DATA_TIME,
+                                      "Time of exposure", time);
+            psFree(time);
+            pmConceptRegister(fpaTime, (pmConceptParseFunc)p_pmConceptParse_TIME,
+                              (pmConceptFormatFunc)p_pmConceptFormat_TIME, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaTime);
+        }
+
+        // FPA.TEMP
+        {
+            psMetadataItem *fpaTemp = psMetadataItemAllocF32("FPA.TEMP", "Temperature of focal plane", NAN);
+            pmConceptRegister(fpaTemp, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaTemp);
+        }
+
+        // FPA.EXPOSURE
+        {
+            psMetadataItem *fpaExposure = psMetadataItemAllocF32("FPA.EXPOSURE",
+                                          "Exposure time (sec)", NAN);
+            pmConceptRegister(fpaExposure, NULL, NULL, false, PM_FPA_LEVEL_FPA);
+            psFree(fpaExposure);
+        }
+
+        // Done with FPA level concepts
+    }
+    if (! conceptsChip) {
+        conceptsChip = psMetadataAlloc();
+        init = true;
+
+        // Install the standard concepts
+
+        // CHIP.XPARITY
+        {
+            psMetadataItem *chipXparity = psMetadataItemAllocS32("CHIP.XPARITY",
+                                          "Orientation in x compared to the rest of the FPA", 0);
+            pmConceptRegister(chipXparity, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
+            psFree(chipXparity);
+        }
+
+        // CHIP.YPARITY
+        {
+            psMetadataItem *chipYparity = psMetadataItemAllocS32("CHIP.YPARITY",
+                                          "Orientation in y compared to the rest of the FPA", 0);
+            pmConceptRegister(chipYparity, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
+            psFree(chipYparity);
+        }
+
+        // CHIP.X0
+        {
+            psMetadataItem *chipX0 = psMetadataItemAllocS32("CHIP.X0", "Position of (0,0) on the FPA", 0);
+            pmConceptRegister(chipX0, (pmConceptParseFunc)p_pmConceptParse_Positions,
+                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CHIP);
+            psFree(chipX0);
+        }
+
+        // CHIP.Y0
+        {
+            psMetadataItem *chipY0 = psMetadataItemAllocS32("CHIP.Y0", "Position of (0,0) on the FPA", 0);
+            pmConceptRegister(chipY0, (pmConceptParseFunc)p_pmConceptParse_Positions,
+                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CHIP);
+            psFree(chipY0);
+        }
+
+        // CHIP.XSIZE
+        {
+            psMetadataItem *chipXsize = psMetadataItemAllocS32("CHIP.XSIZE", "Size of chip (pixels)", 0);
+            pmConceptRegister(chipXsize, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
+            psFree(chipXsize);
+        }
+
+        // CHIP.YSIZE
+        {
+            psMetadataItem *chipYsize = psMetadataItemAllocS32("CHIP.YSIZE", "Size of chip (pixels)", 0);
+            pmConceptRegister(chipYsize, NULL, NULL, true, PM_FPA_LEVEL_CHIP);
+            psFree(chipYsize);
+        }
+
+        // CHIP.TEMP
+        {
+            psMetadataItem *chipTemp = psMetadataItemAllocF32("CHIP.TEMP", "Temperature of chip", NAN);
+            pmConceptRegister(chipTemp, NULL, NULL, false, PM_FPA_LEVEL_CHIP);
+            psFree(chipTemp);
+        }
+
+        // CHIP.ID
+        {
+            psMetadataItem *chipID = psMetadataItemAllocStr("CHIP.ID", "Chip identifier", "");
+            pmConceptRegister(chipID, NULL, NULL, false, PM_FPA_LEVEL_CHIP);
+            psFree(chipID);
+        }
+
+        // Done with chip level concepts
+    }
+    if (! conceptsCell) {
+        conceptsCell = psMetadataAlloc();
+        init = true;
+
+        // Install the standard concepts
+
+        // CELL.GAIN
+        {
+            psMetadataItem *cellGain = psMetadataItemAllocF32("CELL.GAIN", "CCD gain (e/count)", NAN);
+            pmConceptRegister(cellGain, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+            psFree(cellGain);
+        }
+
+        // CELL.READNOISE
+        {
+            psMetadataItem *cellReadnoise = psMetadataItemAllocF32("CELL.READNOISE",
+                                            "CCD read noise (e)", NAN);
+            pmConceptRegister(cellReadnoise, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+            psFree(cellReadnoise);
+        }
+
+        // CELL.SATURATION
+        {
+            psMetadataItem *cellSaturation = psMetadataItemAllocF32("CELL.SATURATION",
+                                             "Saturation level (counts)", NAN);
+            pmConceptRegister(cellSaturation, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+            psFree(cellSaturation);
+        }
+
+        // CELL.BAD
+        {
+            psMetadataItem *cellBad = psMetadataItemAllocF32("CELL.BAD", "Bad level (counts)", NAN);
+            pmConceptRegister(cellBad, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+            psFree(cellBad);
+        }
+
+        // CELL.XPARITY
+        {
+            psMetadataItem *cellXparity = psMetadataItemAllocS32("CELL.XPARITY",
+                                          "Orientation in x compared to the rest of the chip", 0);
+            pmConceptRegister(cellXparity, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+            psFree(cellXparity);
+        }
+
+        // CELL.YPARITY
+        {
+            psMetadataItem *cellYparity = psMetadataItemAllocS32("CELL.YPARITY",
+                                          "Orientation in y compared to the rest of the chip", 0);
+            pmConceptRegister(cellYparity, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+            psFree(cellYparity);
+        }
+
+        // CELL.READDIR
+        {
+            psMetadataItem *cellReaddir = psMetadataItemAllocS32("CELL.READDIR",
+                                          "Read direction, rows=1, cols=2", 0);
+            pmConceptRegister(cellReaddir, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+            psFree(cellReaddir);
+        }
+
+
+        // These (CELL.EXPOSURE and CELL.DARKTIME) used to be READOUT.EXPOSURE and READOUT.DARKTIME, but that
+        // doesn't really make sense at the moment.  Maybe we need to add a "parent" link to the readouts.
+        // But then how are the exposure times REALLY derived?  They're not in the FITS headers, because a
+        // readout is a plane in a 3D image.  We'll have to dream up some additional suffix to specify these,
+        // but for now....
+
+        // CELL.EXPOSURE
+        {
+            psMetadataItem *cellExposure = psMetadataItemAllocF32("CELL.EXPOSURE",
+                                           "Exposure time (sec)", NAN);
+            pmConceptRegister(cellExposure, NULL, NULL, false, PM_FPA_LEVEL_CELL);
+            psFree(cellExposure);
+        }
+
+        // CELL.DARKTIME
+        {
+            psMetadataItem *cellDarktime = psMetadataItemAllocF32("CELL.DARKTIME",
+                                           "Time since flush (sec)", NAN);
+            pmConceptRegister(cellDarktime, NULL, NULL, false, PM_FPA_LEVEL_CELL);
+            psFree(cellDarktime);
+        }
+
+        // CELL.TRIMSEC
+        {
+            psRegion *trimsec = psAlloc(sizeof(psRegion)); // Blank trimsec
+            trimsec->x0 = trimsec->y0 = trimsec->x1 = trimsec->y1 = NAN;
+            psMetadataItem *cellTrimsec = psMetadataItemAllocPtr("CELL.TRIMSEC", PS_DATA_REGION,
+                                          "Trim section", trimsec);
+            psFree(trimsec);
+            pmConceptRegister(cellTrimsec, (pmConceptParseFunc)p_pmConceptParse_CELL_TRIMSEC,
+                              (pmConceptFormatFunc)p_pmConceptFormat_CELL_TRIMSEC, true, PM_FPA_LEVEL_CELL);
+            psFree(cellTrimsec);
+        }
+
+        // CELL.BIASSEC
+        {
+            psList *biassecs = psListAlloc(NULL); // Blank biassecs
+            psMetadataItem *cellBiassec = psMetadataItemAllocPtr("CELL.BIASSEC", PS_DATA_LIST,
+                                          "Bias sections", biassecs);
+            psFree(biassecs);
+            pmConceptRegister(cellBiassec, (pmConceptParseFunc)p_pmConceptParse_CELL_BIASSEC,
+                              (pmConceptFormatFunc)p_pmConceptFormat_CELL_BIASSEC, true, PM_FPA_LEVEL_CELL);
+            psFree(cellBiassec);
+        }
+
+        // CELL.XBIN
+        {
+            psMetadataItem *cellXbin = psMetadataItemAllocS32("CELL.XBIN", "Binning in x", 0);
+            pmConceptRegister(cellXbin, (pmConceptParseFunc)p_pmConceptParse_CELL_Binning,
+                              (pmConceptFormatFunc)p_pmConceptFormat_CELL_XBIN, true, PM_FPA_LEVEL_CELL);
+            psFree(cellXbin);
+        }
+
+        // CELL.YBIN
+        {
+            psMetadataItem *cellYbin = psMetadataItemAllocS32("CELL.YBIN", "Binning in y", 0);
+            pmConceptRegister(cellYbin, (pmConceptParseFunc)p_pmConceptParse_CELL_Binning,
+                              (pmConceptFormatFunc)p_pmConceptFormat_CELL_YBIN, true, PM_FPA_LEVEL_CELL);
+            psFree(cellYbin);
+        }
+
+        // CELL.TIMESYS
+        {
+            psMetadataItem *cellTimesys = psMetadataItemAllocS32("CELL.TIMESYS", "Time system", -1);
+            pmConceptRegister(cellTimesys, (pmConceptParseFunc)p_pmConceptParse_TIMESYS,
+                              (pmConceptFormatFunc)p_pmConceptFormat_TIMESYS, false, PM_FPA_LEVEL_CELL);
+            psFree(cellTimesys);
+        }
+
+        // CELL.TIME
+        {
+            psTime *time = psTimeAlloc(PS_TIME_TAI); // Blank time
+            // Not particularly distinguishing, but should be good enough
+            time->sec = 0;
+            time->nsec = 0;
+            psMetadataItem *cellTime = psMetadataItemAlloc("CELL.TIME", PS_DATA_TIME,
+                                       "Time of exposure", time);
+            psFree(time);
+            pmConceptRegister(cellTime, (pmConceptParseFunc)p_pmConceptParse_TIME,
+                              (pmConceptFormatFunc)p_pmConceptFormat_TIME, false, PM_FPA_LEVEL_CELL);
+            psFree(cellTime);
+        }
+
+        // CELL.X0
+        {
+            psMetadataItem *cellX0 = psMetadataItemAllocS32("CELL.X0", "Position of (0,0) on the chip", 0);
+            pmConceptRegister(cellX0, (pmConceptParseFunc)p_pmConceptParse_Positions,
+                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
+            psFree(cellX0);
+        }
+
+        // CELL.Y0
+        {
+            psMetadataItem *cellY0 = psMetadataItemAllocS32("CELL.Y0", "Position of (0,0) on the chip", 0);
+            pmConceptRegister(cellY0, (pmConceptParseFunc)p_pmConceptParse_Positions,
+                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
+            psFree(cellY0);
+        }
+
+        // CELL.XSIZE
+        {
+            psMetadataItem *cellXsize = psMetadataItemAllocS32("CELL.XSIZE", "Size of cell (pixels)", 0);
+            pmConceptRegister(cellXsize, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+            psFree(cellXsize);
+        }
+
+        // CELL.YSIZE
+        {
+            psMetadataItem *cellYsize = psMetadataItemAllocS32("CELL.YSIZE", "Size of cell (pixels)", 0);
+            pmConceptRegister(cellYsize, NULL, NULL, true, PM_FPA_LEVEL_CELL);
+            psFree(cellYsize);
+        }
+
+        // CELL.XWINDOW
+        {
+            psMetadataItem *cellXwindow = psMetadataItemAllocS32("CELL.XWINDOW",
+                                                                 "Start of cell window (pixels)", 0);
+            pmConceptRegister(cellXwindow, (pmConceptParseFunc)p_pmConceptParse_Positions,
+                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
+            psFree(cellXwindow);
+        }
+
+        // CELL.YWINDOW
+        {
+            psMetadataItem *cellYwindow = psMetadataItemAllocS32("CELL.YWINDOW",
+                                                                 "Start of cell window (pixels)", 0);
+            pmConceptRegister(cellYwindow, (pmConceptParseFunc)p_pmConceptParse_Positions,
+                              (pmConceptFormatFunc)p_pmConceptFormat_Positions, true, PM_FPA_LEVEL_CELL);
+            psFree(cellYwindow);
+        }
+    }
+
+    p_psMemAllocatePersistent(false);
+
+    return init;
+}
+
+void pmConceptsDone(void)
+{
+    psFree(conceptsFPA);
+    conceptsFPA = NULL;
+    psFree(conceptsChip);
+    conceptsChip = NULL;
+    psFree(conceptsCell);
+    conceptsCell = NULL;
+
+    conceptsInitialised = false;
+}
+
+
+// XXX EAM : Paul, please handle the biassec independently so the
+// psList is copied without a verbose warning.  when this is fixed, you can
+// convert the trace back to a log (psMetadata.c:429)
+
+bool pmFPACopyConcepts(pmFPA *target, const pmFPA *source)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+    // Copy FPA concepts
+    target->concepts = psMetadataCopy(target->concepts, source->concepts);
+
+    // Copy chip concepts
+    psArray *targetChips = target->chips; // Chips in target
+    psArray *sourceChips = source->chips; // Chips in source
+    if (targetChips->n != sourceChips->n) {
+        psError(PS_ERR_IO, true, "Number of chips in target (%ld) and source (%ld) differ --- unable to copy "
+                "concepts.\n", targetChips->n, sourceChips->n);
+        return false;
+    }
+    for (int i = 0; i < targetChips->n; i++) {
+        pmChip *targetChip = targetChips->data[i]; // Target chip of interest
+        pmChip *sourceChip = sourceChips->data[i]; // Source chip of interest
+        if (! targetChip || ! sourceChip) {
+            continue;
+        }
+        targetChip->concepts = psMetadataCopy(targetChip->concepts, sourceChip->concepts);
+
+        // Copy cell concepts
+        psArray *targetCells = targetChip->cells; // Cells in target
+        psArray *sourceCells = sourceChip->cells; // Cells in source
+        if (targetCells->n != sourceCells->n) {
+            psError(PS_ERR_IO, true, "Number of cells in target (%ld) and source (%ld) differ for chip %d ---"
+                    " unable to copy concepts.\n", targetCells->n, sourceCells->n, i);
+            return false;
+        }
+        for (int j = 0; j < targetCells->n; j++) {
+            pmCell *targetCell = targetCells->data[j]; // Target chip of interest
+            pmCell *sourceCell = sourceCells->data[j]; // Source chip of interest
+            if (! targetCell || ! sourceCell) {
+                continue;
+            }
+
+            psMetadataIterator *iter = psMetadataIteratorAlloc(sourceCell->concepts, PS_LIST_HEAD, NULL);
+            psMetadataItem *item;       // Item from iteration
+            while ((item = psMetadataGetAndIncrement(iter))) {
+                psMetadataItem *new;    // New item, a copy of the old
+                // Don't copy TRIMSEC or BIASSEC
+                if (strcmp(item->name, "CELL.TRIMSEC") != 0 && strcmp(item->name, "CELL.BIASSEC")) {
+                    new = psMetadataItemCopy(item);
+                } else {
+                    new = psMetadataItemAlloc(item->name, item->type, item->comment, NULL);
+                }
+                psMetadataAddItem(targetCell->concepts, new, PS_LIST_TAIL, PS_META_REPLACE);
+                psFree(new);        // Drop reference
+            }
+            psFree(iter);
+        }
+    }
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConcepts.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConcepts.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConcepts.h	(revision 15358)
@@ -0,0 +1,206 @@
+/* @file pmConcepts.h
+ * @brief Top-level functions for defining, registering, reading and writing concepts
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.14 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONCEPTS_H
+#define PM_CONCEPTS_H
+
+/// @addtogroup Concepts Data Abstraction Concepts
+/// @{
+
+/// Source for concepts when reading and writing.
+///
+/// Since some sources become available at different times from others, we need to provide some specificity to
+/// reading and writing concepts (or we're forced to wait until everything's available, which we don't want to
+/// do).  Concepts may be read from or written to multiple sources at once by OR-ing them.
+typedef enum {
+    PM_CONCEPT_SOURCE_NONE     = 0x00,  ///< No concepts
+    PM_CONCEPT_SOURCE_CELLS    = 0x01,  ///< Concept comes from the camera information
+    PM_CONCEPT_SOURCE_DEFAULTS = 0x02,  ///< Concept comes from defaults
+    PM_CONCEPT_SOURCE_PHU      = 0x04,  ///< Concept comes from PHU
+    PM_CONCEPT_SOURCE_HEADER   = 0x08,  ///< Concept comes from FITS header
+    PM_CONCEPT_SOURCE_DATABASE = 0x10,  ///< Concept comes from database
+    PM_CONCEPT_SOURCE_ALL      = 0xff   ///< All concepts
+} pmConceptSource;
+
+/// Function to call to parse a concept once it has been read
+typedef psMetadataItem* (*pmConceptParseFunc)(const psMetadataItem *concept, ///< Concept to parse
+        const psMetadataItem *pattern, ///< Pattern for parsing
+                                              pmConceptSource source, ///< Source of concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                             );
+
+/// Function to call to format a concept for writing
+typedef psMetadataItem* (*pmConceptFormatFunc)(const psMetadataItem *concept, ///< Concept to format
+                                               pmConceptSource source, ///< Source of concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                              );
+
+/// A "concept" specification
+///
+/// Defines the name, default comment, blank value, and functions to parse (after reading) and format (before
+/// writing) the concept.
+typedef struct
+{
+    psMetadataItem *blank;              ///< Blank value of concept; also contains the name and comment
+    pmConceptParseFunc parse;           ///< Function to call to read the concept
+    pmConceptFormatFunc format;         ///< Function to call to write the concept
+    bool required;                      ///< Is concept required (throw an error on problems)?
+}
+pmConceptSpec;
+
+/// Allocator for pmConceptSpec
+pmConceptSpec *pmConceptSpecAlloc(psMetadataItem *blank, ///< Blank value; contains the name
+                                  pmConceptParseFunc parse, ///< Function to call to parse the concept
+                                  pmConceptFormatFunc format, ///< Function to call to format the concept
+                                  bool required ///< Is concept required?
+                                 );
+
+/// Get whether a particular concept is required
+bool pmConceptGetRequired(const char *name, ///< Name of concept
+                          pmFPALevel level ///< Level at which concept resides
+    );
+
+/// Set whether a particular concept is required
+bool pmConceptSetRequired(const char *name, ///< Name of concept
+                          pmFPALevel level, ///< Level at which concept resides
+                          bool required ///< Whether concept is required or not
+    );
+
+/// Register a new concept for parsing and formatting
+///
+/// Defines a new concept, based on the blank value (with name and default comment), and functions to parse
+/// and format the concept.  The new concept is registered at the specified level (FPA, chip or cell).  If the
+/// parse function is NULL, then a default parse function is used, which performs minimal parsing.  Similarly
+/// for the format function.
+bool pmConceptRegister(psMetadataItem *blank, ///< Blank value; contains the name and default comment
+                       pmConceptParseFunc parse, ///< Function to call to parse the concept, or NULL
+                       pmConceptFormatFunc format, ///< Function to call to format the concept, or NULL
+                       bool required,   ///< Is concept required?
+                       pmFPALevel level ///< Level at which to store concept in the FPA hierarchy
+                      );
+
+/// Get a list of defined concepts for a particular level.
+psList *pmConceptsList(pmFPALevel level);
+
+/// Read the concepts for the given set of fpa, chip, cell
+///
+/// Attempts to read as many concepts as possible from the specified source for the specified FPA, chip and
+/// cell.  That is, it will read chip- and cell-level concepts in addition to fpa-level concepts, if the chip
+/// and cell are provided.
+bool pmConceptsRead(pmFPA *fpa,         ///< FPA for which to read concepts
+                    pmChip *chip,       ///< Chip for which to read concepts, or NULL
+                    pmCell *cell,       ///< Cell for which to read concepts, or NULL
+                    pmConceptSource source, ///< The source of the concepts to read
+                    psDB *db            ///< Database handle
+                   );
+
+/// Set the concepts within the FPA to the blank value
+bool pmConceptsBlankFPA(pmFPA *fpa      ///< FPA for which to set blank concepts
+                       );
+
+/// Read concepts for an FPA; optionally, read concepts at all lower levels.
+///
+/// Once concepts should be available for reading at the FPA-level, this function attempts to read the
+/// concepts from the specified source.  It also allows concepts to be read at lower levels by iterating over
+/// the components.
+bool pmConceptsReadFPA(pmFPA *fpa,      ///< FPA for which to read concepts
+                       pmConceptSource source, ///< Source for concepts
+                       bool propagateDown, ///< Propagate to lower levels?
+                       psDB *db         ///< Database handle
+                      );
+
+/// Write concepts for an FPA; optionally, write concepts at all lower levels.
+///
+/// This function writes all concepts for the FPA to the specified "source".  It also allows concepts to be
+/// written for all lower levels by iterating over the components.
+bool pmConceptsWriteFPA(const pmFPA *fpa,     ///< FPA for which to write concepts
+                        pmConceptSource source, ///< Source for concepts
+                        bool propagateDown, ///< Propagate to lower levels?
+                        psDB *db        ///< Database handle
+                       );
+
+/// Set the concepts within the chip to the blank value
+bool pmConceptsBlankChip(pmChip *chip   ///< FPA for which to set blank concepts
+                        );
+
+/// Read concepts for a chip; optionally, read concepts at the FPA and cell levels.
+///
+/// Once concepts should be available for reading at the FPA-level, this function attempts to read the
+/// concepts from the specified source.  It also allows concepts to be read at the fpa level (through the
+/// parent), and the cell level by iterating over the components.
+bool pmConceptsReadChip(pmChip *chip,   ///< Chip for which to read concepts
+                        pmConceptSource source, ///< Source for concepts
+                        bool propagateUp, ///< Propagate to higher levels?
+                        bool propagateDown, ///< Propagate to lower levels?
+                        psDB *db        ///< Database handle
+                       );
+
+/// Write concepts for a chip; optionally, write concepts at the FPA and cell levels.
+///
+/// This function writes all concepts for the chip to the specified "source".  It also allows concepts to be
+/// written for the FPA, and the cell level by iterating over the components.
+bool pmConceptsWriteChip(const pmChip *chip,  ///< Chip for which to write concepts
+                         pmConceptSource source, ///< Source for concepts
+                         bool propagateUp,///< Propagate to higher levels?
+                         bool propagateDown, ///< Propagate to lower levels?
+                         psDB *db       ///< Database handle
+                        );
+
+/// Set the concepts within the cell to the blank value
+bool pmConceptsBlankCell(pmCell *cell   ///< Cell for which to set blank concepts
+                        );
+
+/// Read concepts for a cell; optionally, read concepts for the parents.
+///
+/// Once concepts should be available for reading at the FPA-level, this function attempts to read the
+/// concepts from the specified source.  It also allows concepts to be read at upper levels through the
+/// parents (note, it would not read concepts for all chips, but only the parent of this cell).
+bool pmConceptsReadCell(pmCell *cell,   ///< Cell for which to read concepts
+                        pmConceptSource source, ///< Source for concepts
+                        bool propagateUp, ///< Propagate to higher levels?
+                        psDB *db        ///< Database handle
+                       );
+
+/// Write concepts for a cell; optionally, write concepts for the parents.
+///
+/// This function writes all concepts for the chip to the specified "source".  It also allows concepts to be
+/// written for the upper levels through the parents (note, it would not write concepts for all chips, but
+/// only the parent of this cell).
+bool pmConceptsWriteCell(const pmCell *cell,  ///< FPA for which to write concepts
+                         pmConceptSource source, ///< Source for concepts
+                         bool propagateUp, ///< Propagate to higher levels?
+                         psDB *db       ///< Database handle
+                        );
+
+/// Initialise the concepts system.
+///
+/// Register the standard concepts, so that concepts may be read and written.  This function is called
+/// automatically the first time the concepts functions are used.
+bool pmConceptsInit(void);
+
+/// Signifies that the user is done with the concepts system.
+///
+/// Frees the registered concepts so there is no memory leak when the user checks "persistent" memory.
+void pmConceptsDone(void);
+
+/// Copy all the concepts within an FPA to another FPA
+///
+/// Iterates over all components of the FPA, and copies the concepts metadata from the source to the target.
+bool pmFPACopyConcepts(pmFPA *target,   ///< The target FPA
+                       const pmFPA *source    ///< The source FPA
+                      );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsAverage.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsAverage.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsAverage.c	(revision 15358)
@@ -0,0 +1,216 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+#include <string.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmConcepts.h"
+#include "pmConceptsAverage.h"
+
+// Update a metadata entry directly
+#define MD_UPDATE(MD, NAME, TYPE, VALUE) \
+{ \
+    psMetadataItem *item = psMetadataLookup(MD, NAME); \
+    item->data.TYPE = VALUE; \
+}
+
+bool pmConceptsAverageFPAs(pmFPA *target, psList *sources)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_INT_POSITIVE(sources->n, false);
+    bool success = true;                // Result of setting everything
+
+    double time      = 0.0;             // Time of observation
+    psTimeType timeSys = 0;             // Time system
+    char *filter     = NULL;            // Filter
+
+    int num = 0;                        // Number of FPAs
+    psListIterator *sourcesIter = psListIteratorAlloc(sources, PS_LIST_HEAD, false); // Iterator for sources
+    pmFPA *fpa = NULL;                  // Source FPA from iteration
+    while ((fpa = psListGetAndIncrement(sourcesIter))) {
+        if (!fpa) {
+            continue;
+        }
+
+        num++;
+
+        psTime *fpaTime = psMetadataLookupPtr(NULL, fpa->concepts, "FPA.TIME");
+        time       += psTimeToMJD(fpaTime);
+        if (num == 1) {
+            timeSys = psMetadataLookupS32(NULL, fpa->concepts, "FPA.TIMESYS");
+            filter = psMetadataLookupStr(NULL, fpa->concepts, "FPA.FILTER");
+        } else {
+            if (timeSys != psMetadataLookupS32(NULL, fpa->concepts, "FPA.TIMESYS")) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Differing FPA.TIMESYS in use: %d vs %d\n",
+                        timeSys, psMetadataLookupS32(NULL, fpa->concepts, "FPA.TIMESYS"));
+                success = false;
+            }
+            if (strcmp(filter, psMetadataLookupStr(NULL, fpa->concepts, "FPA.FILTER")) != 0) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Differing FPA.FILTER in use: %s vs %s\n",
+                        filter, psMetadataLookupStr(NULL, fpa->concepts, "FPA.FILTER"));
+                success = false;
+            }
+        }
+    }
+    psFree(sourcesIter);
+
+    time      /= (double)num;
+
+    MD_UPDATE(target->concepts, "FPA.TIMESYS", S32, timeSys);
+    MD_UPDATE(target->concepts, "FPA.FILTER", str, psStringCopy(filter));
+
+    // FPA.TIME needs special care
+    {
+        psMetadataItem *timeItem = psMetadataLookup(target->concepts, "FPA.TIME");
+        psFree(timeItem->data.V);
+        timeItem->data.V = psTimeFromMJD(time);
+    }
+
+    return success;
+}
+
+
+// Set a variety of concepts in a cell by averaging over several
+// XXX does not properly set XSIZE, YSIZE
+bool pmConceptsAverageCells(pmCell *target, psList *sources, psRegion *trimsec, psRegion *biassec, bool same)
+{
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_INT_POSITIVE(sources->n, false);
+
+    bool success = true;                // Result of setting everything
+    float gain       = 0.0;             // Gain
+    float readnoise  = 0.0;             // Read noise
+    float saturation = INFINITY;        // Saturation level
+    float bad        = -INFINITY;       // Bad level
+    float exposure   = 0.0;             // Exposure time
+    float darktime   = 0.0;             // Dark time
+    double time      = 0.0;             // Time of observation
+    psTimeType timeSys = 0;             // Time system
+    int readdir      = 0;               // Cell read direction
+    int xBin = 0, yBin = 0;             // Binning
+    int x0 = 0, y0 = 0;                 // Offset
+
+    int nCells = 0;                     // Number of cells;
+    psListIterator *sourcesIter = psListIteratorAlloc(sources, PS_LIST_HEAD, false); // Iterator for sources
+    pmCell *cell = NULL;                // Source cell from iteration
+    while ((cell = psListGetAndIncrement(sourcesIter))) {
+        if (!cell) {
+            continue;
+        }
+
+        nCells++;
+        gain       += psMetadataLookupF32(NULL, cell->concepts, "CELL.GAIN");
+        readnoise  += psMetadataLookupF32(NULL, cell->concepts, "CELL.READNOISE");
+        exposure   += psMetadataLookupF32(NULL, cell->concepts, "CELL.EXPOSURE");
+        darktime   += psMetadataLookupF32(NULL, cell->concepts, "CELL.DARKTIME");
+        psTime *cellTime = psMetadataLookupPtr(NULL, cell->concepts, "CELL.TIME");
+        time       += psTimeToMJD(cellTime);
+        if (nCells == 1) {
+            timeSys = psMetadataLookupS32(NULL, cell->concepts, "CELL.TIMESYS");
+            readdir = psMetadataLookupS32(NULL, cell->concepts, "CELL.READDIR");
+            xBin    = psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN");
+            yBin    = psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN");
+
+            if (same) {
+                // Only makes sense to update these if they are the same cell
+                x0  = psMetadataLookupS32(NULL, cell->concepts, "CELL.X0");
+                y0  = psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0");
+            }
+        } else {
+            if (timeSys != psMetadataLookupS32(NULL, cell->concepts, "CELL.TIMESYS")) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Differing CELL.TIMESYS in use: %d vs %d\n",
+                        timeSys, psMetadataLookupS32(NULL, cell->concepts, "CELL.TIMESYS"));
+                success = false;
+            }
+            if (readdir != psMetadataLookupS32(NULL, cell->concepts, "CELL.READDIR")) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Differing CELL.READDIR in use: %d vs %d\n",
+                        readdir, psMetadataLookupS32(NULL, cell->concepts, "CELL.READDIR"));
+                success = false;
+            }
+            if (xBin != psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN")) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Differing CELL.XBIN in use: %d vs %d\n",
+                        xBin, psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN"));
+                success = false;
+            }
+            if (yBin != psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN")) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Differing CELL.YBIN in use: %d vs %d\n",
+                        yBin, psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN"));
+                success = false;
+            }
+            if (same) {
+                if (x0 != psMetadataLookupS32(NULL, cell->concepts, "CELL.X0")) {
+                    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Differing CELL.X0 in use: %d vs %d\n",
+                            x0, psMetadataLookupS32(NULL, cell->concepts, "CELL.X0"));
+                    success = false;
+                }
+                if (y0 != psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0")) {
+                    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Differing CELL.Y0 in use: %d vs %d\n",
+                            y0, psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0"));
+                    success = false;
+                }
+            }
+        }
+
+        float cellSaturation = psMetadataLookupF32(NULL, cell->concepts, "CELL.SATURATION");
+        if (cellSaturation < saturation) {
+            saturation = cellSaturation;
+        }
+        float cellBad = psMetadataLookupF32(NULL, cell->concepts, "CELL.BAD");
+        if (cellBad > bad) {
+            bad = cellBad;
+        }
+    }
+    psFree(sourcesIter);
+
+    gain      /= (float)nCells;
+    readnoise /= (float)nCells;
+    exposure  /= (float)nCells;
+    darktime  /= (float)nCells;
+    time      /= (double)nCells;
+
+    MD_UPDATE(target->concepts, "CELL.GAIN", F32, gain);
+    MD_UPDATE(target->concepts, "CELL.READNOISE", F32, readnoise);
+    MD_UPDATE(target->concepts, "CELL.SATURATION", F32, saturation);
+    MD_UPDATE(target->concepts, "CELL.BAD", F32, bad);
+    MD_UPDATE(target->concepts, "CELL.EXPOSURE", F32, exposure);
+    MD_UPDATE(target->concepts, "CELL.DARKTIME", F32, darktime);
+    MD_UPDATE(target->concepts, "CELL.TIMESYS", S32, timeSys);
+    MD_UPDATE(target->concepts, "CELL.READDIR", S32, readdir);
+    MD_UPDATE(target->concepts, "CELL.XBIN", S32, xBin);
+    MD_UPDATE(target->concepts, "CELL.YBIN", S32, yBin);
+    if (same) {
+        MD_UPDATE(target->concepts, "CELL.X0", S32, x0);
+        MD_UPDATE(target->concepts, "CELL.Y0", S32, y0);
+    }
+
+    // CELL.TIME needs special care
+    {
+        psMetadataItem *timeItem = psMetadataLookup(target->concepts, "CELL.TIME");
+        psFree(timeItem->data.V);
+        timeItem->data.V = psTimeFromMJD(time);
+    }
+
+    // CELL.TRIMSEC needs special care
+    if (trimsec) {
+        psMetadataItem *trimsecItem = psMetadataLookup(target->concepts, "CELL.TRIMSEC");
+        psFree(trimsecItem->data.V);
+        trimsecItem->data.V = psMemIncrRefCounter(trimsec);
+    }
+
+    // CELL.BIASSEC needs special care
+    if (biassec) {
+        psMetadataItem *biassecItem = psMetadataLookup(target->concepts, "CELL.BIASSEC");
+        psFree(biassecItem->data.V);
+        biassecItem->data.V = psMemIncrRefCounter(biassec);
+    }
+
+    return success;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsAverage.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsAverage.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsAverage.h	(revision 15358)
@@ -0,0 +1,57 @@
+/* @file pmConceptsAverage.h
+ * @brief Average the values of multiple concepts
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-08-11 01:05:59 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONCEPTS_AVERAGE_H
+#define PM_CONCEPTS_AVERAGE_H
+
+/// @addtogroup Concepts Data Abstraction Concepts
+/// @{
+
+/// Set a variety of concepts in an FPA by averaging over several
+///
+/// This function averages the values of the following concepts:
+/// FPA.TIME
+/// And ensure the following concepts are consistent:
+/// FPA.FILTER
+/// FPA.TIMESYS
+/// The following concepts could be of interest to the user, but are not treated:
+/// FPA.EXPOSURE
+bool pmConceptsAverageFPAs(pmFPA *target,///< Target FPA
+                           psList *sources ///< List of source FPAs
+    );
+
+/// Set a variety of concepts in a cell by averaging over several
+///
+/// In some instances, we want to combine the values of a concept for several cells into a single concept for
+/// a single cell (e.g., when mosaicking multiple cells into a chip with one "cell").  This function averages
+/// the values of various concepts:
+/// - CELL.GAIN
+/// - CELL.READNOISE
+/// - CELL.EXPOSURE
+/// - CELL.DARKTIME
+/// - CELL.TIME
+/// For other concepts, it ensures the values are consistent:
+/// - CELL.READDIR
+/// - CELL.TIMESYS
+/// - CELL.X0, CELL.Y0
+/// And for others, it takes the "worst" possible value:
+/// - CELL.SATURATION
+/// - CELL.BAD
+/// These concepts are only handled if the cells are all the same cell (mosaicking vs stacking):
+/// - CELL.X0, CELL.Y0
+bool pmConceptsAverageCells(pmCell *target,///< Target cell
+                            psList *sources, ///< List of source cells
+                            psRegion *trimsec, ///< The new trim section
+                            psRegion *biassec, ///< The new bias section
+                            bool same   ///< Are the cells the same cell from different chips?
+                           );
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsPhotcode.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsPhotcode.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsPhotcode.c	(revision 15358)
@@ -0,0 +1,49 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmConceptsPhotcode.h"
+
+psString pmConceptsPhotcodeForView(pmConfig *config, pmFPAfile *file, const pmFPAview *view)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(file, NULL);
+    PS_ASSERT_PTR_NON_NULL(view, NULL);
+
+    if (view->chip < -1) {
+        psError(PS_ERR_IO, true, "Photcodes undefined for FPA: defined by chip\n");
+        return NULL;
+    }
+
+    // select recipe options supplied on command line
+    bool mdok;                          // Status of MD lookup
+    psMetadata *recipe  = psMetadataLookupPtr(&mdok, config->recipes, "PPIMAGE");
+    if (!mdok || !recipe) {
+        psError(PS_ERR_IO, true, "Recipe PPIMAGE not found\n");
+        return NULL;
+    }
+
+    // select photcode rule from recipe
+    char *rule = psMetadataLookupStr(&mdok, recipe, "PHOTCODE.RULE");
+    if (!mdok || !rule || strlen(rule) == 0) {
+        psError(PS_ERR_IO, true, "PHOTCODE.RULE not found in PPIMAGE recipe\n");
+        return NULL;
+    }
+
+    // convert rule to real photcode
+    psString photcode = pmFPAfileNameFromRule(rule, file, view);
+
+    return photcode;
+}
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsPhotcode.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsPhotcode.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsPhotcode.h	(revision 15358)
@@ -0,0 +1,25 @@
+/* @file  pmConceptsPhotcode.h
+ * @brief Generate a photcode from the concepts
+ *
+ * @author Eugene Magnier, IfA
+ *
+ * @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-05-17 21:35:51 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONCEPTS_PHOTCODE_H
+#define PM_CONCEPTS_PHOTCODE_H
+
+/// @addtogroup Concepts Data Abstraction Concepts
+/// @{
+
+/// Return the photcode based on the PHOTCODE.RULE in the PHASE2 recipe
+///
+/// A photometry code ("photcode") is a string that represents the combination of filter and detector (chip).
+/// This functions generates a photcode for a particular chip within the FPA, based on the PHOTCODE.RULE in
+/// the PHASE2 recipe.  Interpolation using the usual syntax (e.g., "{CHIP.NAME}") is permitted.
+psString pmConceptsPhotcodeForView(pmConfig *config, pmFPAfile *file, const pmFPAview *view);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsRead.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsRead.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsRead.c	(revision 15358)
@@ -0,0 +1,503 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include <strings.h>		/* for strn?casecmp */
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmHDUUtils.h"
+#include "pmConcepts.h"
+#include "pmConceptsRead.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// This function gets called for the really boring concepts --- where all you have to do is parse from a
+// header or database and you don't need to muck around with conversions.  There is no similar "formatPlain",
+// since the type is already known.
+static psMetadataItem *parsePlain(psMetadataItem *concept, // The concept to parse
+                                  const psMetadataItem *pattern // The concept pattern
+                                 )
+{
+    assert(concept);
+    assert(pattern);
+
+    switch (pattern->type) {
+    case PS_DATA_STRING: {
+            psString string = psMetadataItemParseString(concept); // Get the string, so I can free it after it
+            // goes on the MetadataItem
+            psMetadataItem *item = psMetadataItemAllocStr(pattern->name, pattern->comment, string);
+            psFree(string);
+            return item;
+        }
+    case PS_DATA_S32:
+        return psMetadataItemAllocS32(pattern->name, pattern->comment, psMetadataItemParseS32(concept));
+    case PS_DATA_F32:
+        return psMetadataItemAllocF32(pattern->name, pattern->comment, psMetadataItemParseF32(concept));
+    case PS_DATA_F64:
+        return psMetadataItemAllocF64(pattern->name, pattern->comment, psMetadataItemParseF64(concept));
+    default:
+        psLogMsg(__func__, PS_LOG_WARN, "Concept %s (%s) is not of a standard type (%x)\n",
+                 pattern->name, pattern->comment, pattern->type);
+        return NULL;
+    }
+}
+
+// Parse a single concept
+static bool conceptParse(pmConceptSpec *spec, // The concept specification
+                         psMetadataItem *concept, // The concept to parse
+                         pmConceptSource source, // The concept source
+                         psMetadata *cameraFormat, // The camera format
+                         psMetadata *target, // The target
+                         const pmFPA *fpa, // The FPA
+                         const pmChip *chip, // The chip
+                         const pmCell *cell // The cell
+                        )
+{
+    assert(spec);
+    assert(cameraFormat);
+    assert(target);
+
+    if (!concept) {
+        psError(PS_ERR_UNKNOWN, true, "Concept is NULL");
+        return false;
+    }
+
+    psMetadataItem *parsed = NULL;  // The parsed concept
+    if (spec->parse) {
+        parsed = spec->parse(concept, spec->blank, source, cameraFormat, fpa, chip, cell);
+    } else {
+        parsed = parsePlain(concept, spec->blank);
+    }
+    if (!parsed) {
+        if (spec->required) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s\n", spec->blank->name);
+            return false;
+        } else {
+            psWarning("Unable to parse concept %s, but concept not marked as required.\n", spec->blank->name);
+            psErrorClear();
+            return true; // XXX return?
+        }
+    }
+
+    psTrace ("psModules.concepts", 3, "parsing concept: %s\n", spec->blank->name);
+
+    // Plug the parsed concept into a new psMetadataItem, so each "concept" has its own version that can
+    // be altered without affecting the others.  Also, so that we maintain the template name and comment.
+    psMetadataItem *cleaned = NULL;     // Item that's been cleaned up --- correct name and comment
+    switch (spec->blank->type) {
+    case PS_DATA_STRING:
+        cleaned = psMetadataItemAllocStr(spec->blank->name, spec->blank->comment, parsed->data.V);
+        break;
+    case PS_DATA_S32:
+        cleaned = psMetadataItemAllocS32(spec->blank->name, spec->blank->comment, parsed->data.S32);
+        break;
+    case PS_DATA_F32:
+        cleaned = psMetadataItemAllocF32(spec->blank->name, spec->blank->comment, parsed->data.F32);
+        break;
+    case PS_DATA_F64:
+        cleaned = psMetadataItemAllocF64(spec->blank->name, spec->blank->comment, parsed->data.F64);
+        break;
+    default:
+        cleaned = psMetadataItemAlloc(spec->blank->name, parsed->type, spec->blank->comment,
+                                      parsed->data.V);
+    }
+    psFree(parsed);
+    psMetadataAddItem(target, cleaned, PS_LIST_TAIL, PS_META_REPLACE);
+    psFree(cleaned);                 // Drop reference
+    return true;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool p_pmConceptsReadFromCells(psMetadata *target, const psMetadata *specs, const pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(specs, false);
+    PS_ASSERT_PTR_NON_NULL(target, false);
+    if (!cell) {
+        psError(PS_ERR_UNKNOWN, true, "cell is NULL");
+        return false;
+    }
+
+    pmHDU *hdu = pmHDUGetLowest(NULL, NULL, cell); // The HDU at the lowest level
+    if (!hdu) {
+        psError(PS_ERR_UNKNOWN, true, "Can't find HDU for cell");
+        return false;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    psMetadata *cellConfig = cell->config; // The camera configuration for this cell
+    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *specItem = NULL;    // Item from the specs metadata
+    bool status = true;                 // Status of reading concepts
+    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+        pmConceptSpec *spec = specItem->data.V; // The specification
+        psString name = specItem->name; // The concept name
+        psMetadataItem *conceptItem = psMetadataLookup(cellConfig, name); // The concept, or NULL
+        psTrace("psModules.concepts", 10, "%s: %p\n", name, conceptItem);
+        if (conceptItem) {
+            if (conceptItem->type == PS_DATA_STRING) {
+                // Check the SOURCE
+                psString nameSource = NULL; // String with the concept name and ".SOURCE" added
+                psStringAppend(&nameSource, "%s.SOURCE", name);
+                bool mdok = true;       // Status of MD lookup
+                psString source = psMetadataLookupStr(&mdok, cell->config, nameSource); // The source
+                psFree(nameSource);
+                if (mdok && source && strlen(source) > 0 && strcasecmp(source, "VALUE") == 0) {
+                    if (!conceptParse(spec, conceptItem, PM_CONCEPT_SOURCE_CELLS,
+                                      cameraFormat, target, NULL, NULL, cell)) {
+                        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s from camera "
+                                "configuration\n", name);
+                        status = false;
+                    }
+                } else if (source && (strlen(source) == 0 || strcasecmp(source, "HEADER") != 0)) {
+                    // We leave "HEADER" to pmConceptsReadFromHeader
+                    psError(PS_ERR_IO, true, "%s isn't HEADER or VALUE --- can't read %s\n", source,
+                            name);
+                    continue;
+                }
+            } else {
+                // Another type --- should be OK
+                if (!conceptParse(spec, conceptItem, PM_CONCEPT_SOURCE_CELLS,
+                                  cameraFormat, target, NULL, NULL, cell)) {
+                    psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s from camera "
+                            "configuration.\n", name);
+                    status = false;
+                }
+            }
+        }
+    }
+    psFree(specsIter);
+    return status;
+}
+
+
+bool p_pmConceptsReadFromDefaults(psMetadata *target, const psMetadata *specs,
+                                  const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(specs, false);
+    PS_ASSERT_PTR_NON_NULL(target, false);
+
+    psTrace("psModules.concepts", 3, "Reading concepts from defaults...\n");
+
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+    if (!hdu) {
+        // We read the defaults for all the HDUs we could find
+        return true;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *defaults = psMetadataLookupMetadata(&mdok, cameraFormat, "DEFAULTS"); // The DEFAULTS spec
+    if (!mdok || !defaults) {
+        psError(PS_ERR_IO, true, "Failed to find \"DEFAULTS\"");
+        return false;
+    }
+    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *specItem = NULL;    // Item from the specs metadata
+    bool status = true;                 // Status of reading concepts
+    psErrorClear();   // we're going to declare all errors "old" => won't clear stack
+    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+        pmConceptSpec *spec = specItem->data.V; // The specification
+        psString name = specItem->name; // The concept name
+        psMetadataItem *conceptItem = psMetadataLookup(defaults, name); // The concept, or NULL
+        psTrace("psModules.concepts", 10, "%s: %p\n", name, conceptItem);
+        if (conceptItem && conceptItem->type == PS_DATA_METADATA) {
+            psTrace("psModules.concepts", 5, "%s is of type METADATA.\n", name);
+            // Check for DEPEND
+            psMetadata *dependMenu = conceptItem->data.V; // The DEPEND menu
+            psString depend = NULL; // The CONCEPT.DEPEND
+            psStringAppend(&depend, "%s.DEPEND", name);
+            const char *dependConcept = psMetadataLookupStr(&mdok, defaults, depend); // The concept name
+            if (!mdok || !dependConcept || strlen(dependConcept) == 0) {
+                psError(PS_ERR_IO, true, "Unable to parse %s: couldn't find %s in DEFAULTS.\n", name,
+                        depend);
+                psFree(depend);
+                continue;
+            }
+            psFree(depend);
+            // Now look up the depend value
+            psMetadataItem *dependValue = NULL; // The value of the concept we're looking up
+            if (cell) {
+                dependValue = psMetadataLookup(cell->concepts, dependConcept);
+            }
+            if (chip && !dependValue) {
+                dependValue = psMetadataLookup(chip->concepts, dependConcept);
+            }
+            if (fpa && !dependValue) {
+                dependValue = psMetadataLookup(chip->concepts, dependConcept);
+            }
+            if (!dependValue) {
+                psError(PS_ERR_IO, true, "Unable to find %s to resolve %s in DEFAULTS.\n",
+                        dependConcept, name);
+                continue;
+            }
+            if (dependValue->type != PS_DATA_STRING) {
+                psError(PS_ERR_BAD_PARAMETER_TYPE, true, "%s is required to resolve %s in DEFAULTS, "
+                        "but it is not of type STRING.\n", dependConcept, name);
+                continue;
+            }
+            const char *dependKey = dependValue->data.V; // The key to the DEPEND menu
+            psTrace("psModules.concepts", 7, "%s.DEPEND resolves to %s....\n", name, dependKey);
+
+            conceptItem = psMetadataLookup(dependMenu, dependKey);
+            // Now we can parse this as we would ordinarily
+        }
+        if (conceptItem && !conceptParse(spec, conceptItem, PM_CONCEPT_SOURCE_DEFAULTS,
+                                         cameraFormat, target, fpa, chip, cell)) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s from DEFAULTS.\n", name);
+            status = false;
+        }
+    }
+    psFree(specsIter);
+    return status;
+}
+
+
+bool p_pmConceptsReadFromHeader(psMetadata *target, const psMetadata *specs,
+                                const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(specs, false);
+    PS_ASSERT_PTR_NON_NULL(target, false);
+
+    pmHDU *hduLow = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+    if (!hduLow) {
+        // We read the defaults for all the HDUs we could find
+        return true;
+    }
+    pmHDU *hduHigh = pmHDUGetHighest(fpa, chip, cell); // The HDU at the highest level
+    if (!hduHigh) {
+        psError(PS_ERR_UNKNOWN, true, "Can't find HDU at the highest level");
+        return false;
+    }
+    assert(hduLow->format == hduHigh->format); // Just in case....
+    psMetadata *cameraFormat = hduLow->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *transSpec = psMetadataLookupMetadata(&mdok, cameraFormat, "TRANSLATION"); // The TRANSLATION spec
+    if (!mdok || !transSpec) {
+        psError(PS_ERR_IO, true, "Failed to find \"TRANSLATION\"");
+        return false;
+    }
+
+    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *specItem = NULL;    // Item from the specs metadata
+    bool status = true;                 // Status of reading concepts
+    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+        pmConceptSpec *spec = specItem->data.V; // The specification
+        psString name = specItem->name; // The concept name
+        psMetadataItem *headerItem = NULL; // The value of the concept from the header
+
+        // First check the cell configuration
+        if (cell && cell->config) {
+            psMetadataItem *conceptItem = psMetadataLookup(cell->config, name); // The concept, or NULL
+            if (conceptItem) {
+                // Check the SOURCE
+                psString nameSource = NULL; // String with the concept name and ".SOURCE" added
+                psStringAppend(&nameSource, "%s.SOURCE", name);
+                psString source = psMetadataLookupStr(&mdok, cell->config, nameSource); // The source
+                psFree(nameSource);
+                if (mdok && strlen(source) && strcasecmp(source, "HEADER") == 0) {
+                    if (hduLow->header) {
+                        headerItem = psMetadataLookup(hduLow->header, conceptItem->data.V);
+                    }
+                    if (!headerItem && hduHigh != hduLow && hduHigh->header) {
+                        headerItem = psMetadataLookup(hduHigh->header, conceptItem->data.V);
+                    }
+                    psMemIncrRefCounter(headerItem);
+                }
+                // Leave the error handling to pmConceptsFromCamera, which should already have been called
+            }
+        }
+
+        if (! headerItem) {
+            psString keywords = psMetadataLookupStr(&mdok, transSpec, name); // The FITS keywords
+            if (mdok && strlen(keywords) > 0) {
+                // In case there are multiple headers
+                psList *keys = psStringSplit(keywords, " ,;", true); // List of keywords
+                if (keys->n == 1) {
+                    // Only one key --- proceed as usual
+                    if (hduLow->header) {
+                        headerItem = psMetadataLookup(hduLow->header, keywords);
+                    }
+                    if (!headerItem && hduHigh != hduLow && hduHigh->header) {
+                        headerItem = psMetadataLookup(hduHigh->header, keywords);
+                    }
+                    psMemIncrRefCounter(headerItem);
+                } else {
+                    psListIterator *keysIter = psListIteratorAlloc(keys, PS_LIST_HEAD, false); // Iterator
+                    psString key = NULL; // Item from iteration
+                    psList *values = psListAlloc(NULL); // List containing the values
+                    while ((key = psListGetAndIncrement(keysIter))) {
+                        psMetadataItem *value = NULL;
+                        if (hduLow->header) {
+                            value = psMetadataLookup(hduLow->header, key);
+                        }
+                        if (!value && hduHigh != hduLow && hduHigh->header) {
+                            value = psMetadataLookup(hduHigh->header, key);
+                        }
+                        if (value) {
+                            psListAdd(values, PS_LIST_TAIL, value);
+                        } else {
+                            psWarning("Unable to find header %s --- assuming value is NULL", key);
+                        }
+                    }
+                    psFree(keysIter);
+                    headerItem = psMetadataItemAlloc(name, PS_DATA_LIST, specItem->comment, values);
+                    psFree(values);
+                }
+                psFree(keys);
+            }
+        }
+
+        // This will also clean up the name
+        if (headerItem) {
+            if (!conceptParse(spec, headerItem, PM_CONCEPT_SOURCE_HEADER,
+                              cameraFormat, target, fpa, chip, cell)) {
+                psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s from header.\n", name);
+                status = false;
+            }
+        }
+        psTrace("psModules.concepts", 10, "%s: %p\n", name, headerItem);
+        psFree(headerItem);
+    }
+    psFree(specsIter);
+    return status;
+}
+
+
+// XXX --- the below code has NOT been tested!
+bool p_pmConceptsReadFromDatabase(psMetadata *target, const psMetadata *specs,
+                                  const pmFPA *fpa, const pmChip *chip, const pmCell *cell, psDB *db)
+{
+    PS_ASSERT_PTR_NON_NULL(specs, false);
+    PS_ASSERT_PTR_NON_NULL(target, false);
+
+    #ifndef HAVE_PSDB
+
+    return true;
+    #else
+
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+    if (!hdu) {
+        // We read the database for all the HDUs we could find
+        return true;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *dbSpec = psMetadataLookupMetadata(&mdok, cameraFormat, "DATABASE"); // The DATABASE spec
+    if (!mdok || !dbSpec) {
+        return true;
+    }
+
+    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *specItem = NULL;    // Item from the specs metadata
+    bool status = true;                 // Status of reading concepts
+    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+        pmConceptSpec *spec = specItem->data.V; // The specification
+        psString name = specItem->name; // The concept name
+
+        psMetadata *dbLookup = psMetadataLookupMetadata(&mdok, dbSpec, name);
+        if (mdok && dbLookup) {
+            const char *tableName = psMetadataLookupStr(&mdok, dbLookup, "TABLE"); // Table name
+            // Names of the "where" columns
+            const char *givenCols = psMetadataLookupStr(&mdok, dbLookup, "GIVENDBCOL");
+            // Values of the "where" columns
+            const char *givenPS = psMetadataLookupStr(&mdok, dbLookup, "GIVENPS");
+
+            // Now, need to get the "given"s
+            if (strlen(givenCols) > 0 || strlen(givenPS) > 0) {
+                psList *cols = psStringSplit(givenCols, ",;", true); // List of column names
+                psList *values = psStringSplit(givenPS, ",;", true); // List of value names for the columns
+                psMetadata *selection = psMetadataAlloc(); // The stuff to select in the DB
+                if (cols->n != values->n) {
+                    psLogMsg(__func__, PS_LOG_WARN,
+                             "The GIVENDBCOL and GIVENPS entries for %s do not have "
+                             "the same number of entries --- ignored.\n", name);
+                } else {
+                    // Iterators for the lists
+                    psListIterator *colsIter = psListIteratorAlloc(cols, PS_LIST_HEAD, false);
+                    psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false);
+                    char *column = NULL;    // Name of the column
+                    while ((column = psListGetAndIncrement(colsIter))) {
+                        char *dependName = psListGetAndIncrement(valuesIter); // Name for the value
+                        if (!strlen(column) || !strlen(name)) {
+                            psLogMsg(__func__, PS_LOG_WARN, "One of the columns or value names for %s is "
+                                     " empty --- ignored.\n", name);
+                        } else {
+                            // Search for the value name
+                            psMetadataItem *item = NULL; // The value
+                            if (!item && cell) {
+                                item = psMetadataLookup(cell->concepts, dependName);
+                            }
+                            if (!item && chip) {
+                                item = psMetadataLookup(chip->concepts, dependName);
+                            }
+                            if (!item && fpa) {
+                                item = psMetadataLookup(fpa->concepts, dependName);
+                            }
+                            if (! item) {
+                                psLogMsg(__func__, PS_LOG_ERROR, "Unable to find the value name %s for DB"
+                                         " lookup on %s --- ignored.\n", dependName, name);
+                            } else {
+                                // We need to create a new psMetadataItem.  I don't think we can't
+                                // simply hack the existing one, since that could conceivably cause
+                                // memory leaks
+                                psMetadataItem *newItem = psMetadataItemAlloc(name, item->type,
+                                                          item->comment,
+                                                          item->data.V);
+                                psMetadataAddItem(selection, newItem, PS_LIST_TAIL, PS_META_REPLACE);
+                                psFree(newItem);
+                            }
+                        }
+                        psFree(dependName);
+                        psFree(column);
+                    } // Iterating through the columns
+                    psFree(colsIter);
+                    psFree(valuesIter);
+
+                    psArray *dbResult = psDBSelectRows(db, tableName, selection, 2); // Lookup result
+                    // Note that we use limit=2 in order to test if there are multiple rows returned
+
+                    psMetadataItem *conceptItem = NULL; // The final result of the DB lookup
+                    if (dbResult->n == 0) {
+                        psLogMsg(__func__, PS_LOG_WARN,
+                                 "Unable to find any rows in DB for %s --- ignored\n", name);
+                    } else {
+                        if (dbResult-> n > 1) {
+                            psLogMsg(__func__, PS_LOG_WARN,
+                                     "Multiple rows returned in DB lookup for %s --- "
+                                     " using the first one only.\n", name);
+                        }
+                        conceptItem = (psMetadataItem*)dbResult->data[0];
+                    }
+
+                    // Now we have the result
+                    if (!conceptParse(spec, conceptItem, PM_CONCEPT_SOURCE_DATABASE,
+                                      cameraFormat, target, fpa, chip, cell)) {
+                        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to parse concept %s from database.\n",
+                                name);
+                        status = false;
+                    }
+
+                }
+                psFree(cols);
+                psFree(values);
+            }
+        } // Doing the "given"s.
+
+    } // Iterating through the concept specifications
+    psFree(specsIter);
+
+    return status;
+    #endif
+}
+
+
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsRead.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsRead.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsRead.h	(revision 15358)
@@ -0,0 +1,60 @@
+/* @file  pmConceptsRead.h
+ * @brief Reading concepts from a variety of sources.
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONCEPTS_READ_H
+#define PM_CONCEPTS_READ_H
+
+/// Read concepts from the camera format file's CELLS.
+///
+/// Examines the CELLS metadata in the camera format file
+/// for the current type of cell, and sucks in the concepts defined there.
+/// This is a useful way of defining concepts that vary depending on the
+/// type of the cell.
+bool p_pmConceptsReadFromCells(psMetadata *target, ///< Place into which to read the concepts
+                               const psMetadata *specs, ///< The concept specifications
+                               const pmCell *cell ///< The cell
+                              );
+
+/// Read concepts from the DEFAULTS in the camera format file.
+///
+/// Examines the DEFAULTS metadata in the camera format file
+/// for concepts in the specs, and imports them into the target.
+bool p_pmConceptsReadFromDefaults(psMetadata *target, // Place into which to read the concepts
+                                  const psMetadata *specs, // The concept specifications
+                                  const pmFPA *fpa, // The FPA
+                                  const pmChip *chip, // The chip
+                                  const pmCell *cell // The cell
+                                 );
+
+/// Read concepts from the header TRANSLATION in the camera format file.
+///
+/// Examines the TRANSLATION metadata in the camera format file
+/// for concepts in the specs, and imports them into the target.
+bool p_pmConceptsReadFromHeader(psMetadata *target, // Place into which to read the concepts
+                                const psMetadata *specs, // The concept specifications
+                                const pmFPA *fpa, // The FPA
+                                const pmChip *chip, // The chip
+                                const pmCell *cell  // The cell
+                               );
+
+/// Read concepts from the header DATABASE in the camera format file.
+///
+/// Examines the DATABASE metadata in the camera format file
+/// for concepts in the specs, and imports them into the target.
+/// WARNING: This function has not been tested; use at your own risk!
+bool p_pmConceptsReadFromDatabase(psMetadata *target, // Place into which to read the concepts
+                                  const psMetadata *specs, // The concept specifications
+                                  const pmFPA *fpa, // The FPA
+                                  const pmChip *chip, // The chip
+                                  const pmCell *cell,  // The cell
+                                  psDB *db // The database handle
+                                 );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsStandard.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsStandard.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsStandard.c	(revision 15358)
@@ -0,0 +1,1181 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>            /* for strn?casecmp */
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmConcepts.h"
+#include "pmConceptsRead.h"
+#include "pmConceptsWrite.h"
+#include "pmConceptsStandard.h"
+
+// The functions in this file are intended to be called solely within the psModules concepts code.  For this
+// reason, they use "assert" instead of the PS_ASSERT_WHATEVER functions --- if there's a problem, then
+// there's a BIG problem that affects all of the code.
+
+#define COMPARE_REGIONS(a,b) (((a)->x0 == (b)->x0 && \
+                               (a)->x1 == (b)->x1 && \
+                               (a)->y0 == (b)->y0 && \
+                               (a)->y1 == (b)->y1) ? true : false)
+
+#define TYPE_CASE(assign, item, TYPE) \
+case PS_TYPE_##TYPE: \
+assign = item->data.TYPE; \
+break;
+
+
+
+static double defaultCoordScaling(const psMetadataItem *pattern)
+{
+    if (strcmp(pattern->name, "FPA.RA") == 0) {
+        psWarning("Assuming format for %s is HOURS.\n", pattern->name);
+        return M_PI / 12.0;
+    }
+    if (strcmp(pattern->name, "FPA.DEC") == 0) {
+        psWarning("Assuming format for %s is DEGREES.\n", pattern->name);
+        return M_PI / 180.0;
+    }
+    psAbort("Should never ever get here.\n");
+    return NAN;
+}
+
+// FPA.FILTER
+psMetadataItem *p_pmConceptParse_FPA_FILTER(const psMetadataItem *concept,
+                                            const psMetadataItem *pattern,
+                                            pmConceptSource source,
+                                            const psMetadata *cameraFormat,
+                                            const pmFPA *fpa,
+                                            const pmChip *chip,
+                                            const pmCell *cell)
+{
+    assert(concept);
+    assert(pattern);
+    assert(fpa);
+    assert(fpa->camera);
+
+    if (concept->type != PS_DATA_STRING) {
+        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Type for %s (%x) is not STR\n",
+                pattern->name, concept->type);
+        return NULL;
+    }
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *filters = psMetadataLookupMetadata(&mdok, fpa->camera, "FILTER.ID");
+    if (!mdok || !filters) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find FILTER.ID in camera configuration.\n");
+        return NULL;
+    }
+
+    // the metadata is in the format (internal) STR (external)
+    // do a reverse lookup to get the internal name
+    psMetadataIterator *iter = psMetadataIteratorAlloc(filters, PS_LIST_HEAD, NULL); // Iterator for filters
+    psMetadataItem *item;               // Item from iteration
+    char *name = NULL;                  // The winning name
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        if (item->type != PS_DATA_STRING) {
+            psWarning("Type for %s (%x) in FILTER.ID in camera configuration is not STR\n",
+                      item->name, item->type);
+            continue;
+        }
+        if (strcmp(item->data.str, concept->data.str) == 0) {
+            name = item->name;
+            break;
+        }
+    }
+    psFree(iter);
+    if (!name) {
+        psError(PS_ERR_UNEXPECTED_NULL, false,
+                "Unable to find any filter matching %s in FILTER.ID in camera configuration.\n",
+                concept->data.str);
+        return NULL;
+    }
+
+    return psMetadataItemAllocStr(pattern->name, pattern->comment, name);
+}
+
+psMetadataItem *p_pmConceptFormat_FPA_FILTER(const psMetadataItem *concept,
+                                             pmConceptSource source,
+                                             const psMetadata *cameraFormat,
+                                             const pmFPA *fpa,
+                                             const pmChip *chip,
+                                             const pmCell *cell)
+{
+    assert(concept);
+    assert(fpa);
+    assert(fpa->camera);
+
+    if (concept->type != PS_DATA_STRING) {
+        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Type for %s (%x) is not STR\n",
+                concept->name, concept->type);
+        return NULL;
+    }
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *filters = psMetadataLookupMetadata(&mdok, fpa->camera, "FILTER.ID");
+    if (!mdok || !filters) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find FILTER.ID in camera configuration.\n");
+        return NULL;
+    }
+
+    const char *key = concept->data.str;        // The name to look up
+    if (!key || strlen(key) == 0) {
+        return psMetadataItemAllocStr(concept->name, concept->comment, NULL);
+    }
+
+    // the metadata is in the format (internal) STR (external)
+    // find the first internal name that matches
+    psMetadataItem *item = psMetadataLookup (filters, key);
+    if (!item) {
+        psError(PS_ERR_UNEXPECTED_NULL, true,
+                "Unable to find %s in FILTER.ID in camera configuration.\n", key);
+        return NULL;
+    }
+
+    if (item->type == PS_DATA_STRING) {
+        return psMetadataItemAllocStr(concept->name, concept->comment, item->data.V);
+    }
+
+    if (item->type == PS_DATA_METADATA_MULTI) {
+        psMetadataItem *entry = psListGet (item->data.list, PS_LIST_HEAD);
+        if (!entry) {
+            psError(PS_ERR_UNEXPECTED_NULL, true,
+                    "List for %s in FILTER.ID in camera configuration is empty.\n", key);
+            return NULL;
+        }
+        return psMetadataItemAllocStr(concept->name, concept->comment, entry->data.V);
+    }
+
+    psError(PS_ERR_UNEXPECTED_NULL, true,
+            "Unable to find %s in FILTER.ID in camera configuration.\n", key);
+    return NULL;
+}
+
+// FPA.RA and FPA.DEC
+psMetadataItem *p_pmConceptParse_FPA_Coords(const psMetadataItem *concept,
+                                            const psMetadataItem *pattern,
+                                            pmConceptSource source,
+                                            const psMetadata *cameraFormat,
+                                            const pmFPA *fpa,
+                                            const pmChip *chip,
+                                            const pmCell *cell)
+{
+    assert(concept);
+    assert(pattern);
+    assert(cameraFormat);
+
+    double coords = NAN;                // The coordinates
+    switch (concept->type) {
+      case PS_TYPE_F32:
+        coords = concept->data.F32;
+        break;
+      case PS_TYPE_F64:
+        coords = concept->data.F64;
+        break;
+      case PS_DATA_STRING:
+        // Sexagesimal format
+        {
+            int big, medium;
+            float small;
+            // XXX: Upgrade path is to allow dd:mm.mmm
+            if (sscanf(concept->data.V, "%d:%d:%f", &big, &medium, &small) != 3 &&
+                sscanf(concept->data.V, "%d %d %f", &big, &medium, &small) != 3)
+            {
+                psError(PS_ERR_UNKNOWN, true, "Cannot interpret %s: %s\n", pattern->name, concept->data.str);
+                break;
+            }
+            coords = abs(big) + (float)medium/60.0 + small/3600.0;
+            if (big < 0)
+            {
+                coords *= -1.0;
+            }
+        }
+        break;
+      default:
+        psError(PS_ERR_UNKNOWN, true, "%s concept is of an unexpected type: %x\n",
+                pattern->name, concept->type);
+        return NULL;
+    }
+
+    // How to interpret the coordinates
+    bool mdok = true;           // Status of MD lookup
+    psMetadata *formats = psMetadataLookupMetadata(&mdok, cameraFormat, "FORMATS");
+    if (mdok && formats) {
+        psString format = psMetadataLookupStr(&mdok, formats, pattern->name);
+        if (mdok && strlen(format) > 0) {
+            if (strcasecmp(format, "HOURS") == 0) {
+                coords *= M_PI / 12.0;
+            } else if (strcasecmp(format, "DEGREES") == 0) {
+                coords *= M_PI / 180.0;
+            } else if (strcasecmp(format, "RADIANS") == 0) {
+                // No action required
+            } else {
+                coords *= defaultCoordScaling(pattern);
+            }
+        } else {
+            coords *= defaultCoordScaling(pattern);
+        }
+    } else {
+        coords *= defaultCoordScaling(pattern);
+    }
+
+    return psMetadataItemAllocF64(pattern->name, pattern->comment, coords);
+}
+
+// FPA.RA and FPA.DEC
+psMetadataItem *p_pmConceptFormat_FPA_Coords(const psMetadataItem *concept,
+                                             pmConceptSource source,
+                                             const psMetadata *cameraFormat,
+                                             const pmFPA *fpa,
+                                             const pmChip *chip,
+                                             const pmCell *cell)
+{
+    assert(concept);
+    assert(cameraFormat);
+
+    double coords = concept->data.F64;  // The coordinates
+
+    if (!isfinite(coords)) {
+        return psMetadataItemAllocF32(concept->name, concept->comment, NAN);
+    }
+
+    // How to interpret the coordinates
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *formats = psMetadataLookupMetadata(&mdok,
+                                                   cameraFormat,
+                                                   "FORMATS");
+    if (mdok && formats) {
+        psString format = psMetadataLookupStr(&mdok,formats, concept->name);
+        if (mdok && strlen(format) > 0) {
+            if (strcasecmp(format, "HOURS") == 0) {
+                coords /= M_PI / 12.0;
+            } else if (strcasecmp(format, "DEGREES") == 0) {
+                coords /= M_PI / 180.0;
+            } else if (strcasecmp(format, "RADIANS") == 0) {
+                // No action required
+            } else {
+                coords /= defaultCoordScaling(concept);
+            }
+        } else {
+            coords /= defaultCoordScaling(concept);
+        }
+    } else {
+        coords /= defaultCoordScaling(concept);
+    }
+
+    // We choose to write sexagesimal format
+    int big, medium;                    // Degrees and minutes
+    float small;                        // Seconds
+    bool negative = (coords < 0);       // Are we working below zero?
+    coords = fabs(coords);
+    big = (int)abs(coords);
+    coords -= big;
+    medium = 60.0 * coords;
+    coords -= medium / 60.0;
+    small = 3600.0 * coords;
+    if (negative) {
+        big *= -1;
+    }
+    psString coordString = NULL;        // String with the coordinates in sexagesimal format
+    psStringAppend(&coordString, "%d:%02d:%05.2f", big, medium, small);
+    psMetadataItem *coordItem = psMetadataItemAllocStr(concept->name, concept->comment, coordString);
+    psFree(coordString);
+
+    return coordItem;
+}
+
+
+psMetadataItem *p_pmConceptParse_CELL_TRIMSEC(const psMetadataItem *concept,
+                                              const psMetadataItem *pattern,
+                                              pmConceptSource source,
+                                              const psMetadata *cameraFormat,
+                                              const pmFPA *fpa,
+                                              const pmChip *chip,
+                                              const pmCell *cell)
+{
+    assert(concept);
+    assert(cell);
+    assert(pattern);
+
+    int xParity = 0;
+    int yParity = 0;
+    psRegion *trimsec = psRegionAlloc(0, 0, 0, 0);
+
+    if (concept->type != PS_DATA_STRING) {
+        psError(PS_ERR_UNKNOWN, true, "CELL.TRIMSEC after read is not of type STR (%x)\n", concept->type);
+        psFree(trimsec);
+        return NULL;
+    } else {
+        // allow for x and y flips in regions
+        *trimsec = psRegionAndParityFromString(&xParity, &yParity, concept->data.V);
+    }
+
+    // Need to correct for binning when CELL.TRIMSEC are specified immutably in the CELLS
+    if (source == PM_CONCEPT_SOURCE_CELLS) {
+        psMetadataAddBool(cell->concepts, PS_LIST_TAIL, "CELL.TRIMSEC.UPDATE", 0,
+                          "Need to update CELL.TRIMSEC when the binning is available.",
+                          true);
+    }
+
+    psMetadataItem *item = psMetadataItemAllocPtr(pattern->name, PS_DATA_REGION, pattern->comment, trimsec);
+    psFree(trimsec);
+    return item;
+}
+
+
+psList *p_pmConceptParseRegions(const char *region)
+{
+    psList *list = psListAlloc(NULL);   // List of regions
+    if (!region || strlen(region) == 0) {
+        // Empty list
+        return list;
+    }
+
+    // a single BIASSEC is of the form [AAAA]
+    // we may have multiple BIASSEC entries separated by space, commas, or semicolons
+    int xParity = 0, yParity = 0;       // Parity of region
+    char *p = strchr (region, '[');
+    while (p) {
+        char *q = strchr (p, ']');
+        if (!q) {
+            break;
+        }
+        char *regionString = psStringAlloc(q - p + 2);
+        strncpy (regionString, p, q - p + 1);
+        regionString[q - p + 1] = 0;
+
+        psRegion *region = psAlloc(sizeof(psRegion)); // The region
+        *region = psRegionAndParityFromString(&xParity, &yParity, regionString);
+        psListAdd(list, PS_LIST_TAIL, region);
+        psFree(region);           // Drop reference
+        psFree(regionString);     // Drop reference
+
+        p = strchr (q, '[');
+    }
+
+    return list;
+}
+
+psMetadataItem *p_pmConceptParse_CELL_BIASSEC(const psMetadataItem *concept,
+                                              const psMetadataItem *pattern,
+                                              pmConceptSource source,
+                                              const psMetadata *cameraFormat,
+                                              const pmFPA *fpa,
+                                              const pmChip *chip,
+                                              const pmCell *cell)
+{
+    assert(concept);
+    assert(cell);
+    assert(pattern);
+
+    psList *biassecs; // List of bias sections
+
+    switch (concept->type) {
+      case PS_DATA_STRING: {
+          biassecs = p_pmConceptParseRegions(concept->data.V);
+          break;
+      }
+      case PS_DATA_LIST: {
+          biassecs = psListAlloc(NULL);
+          psList *regions = concept->data.V; // The list of regions
+          psListIterator *regionsIter = psListIteratorAlloc(regions, PS_LIST_HEAD, false); // Iterator
+          psMetadataItem *regionItem = NULL; // Item from list iteration
+          while ((regionItem = psListGetAndIncrement(regionsIter))) {
+              if (regionItem->type != PS_DATA_STRING) {
+                  psWarning("CELL.BIASSEC member is not of type STR --- ignored.\n");
+                  continue;
+              }
+              int xParity = 0;
+              int yParity = 0;
+              psRegion *region = psAlloc(sizeof(psRegion)); // The region
+              *region = psRegionAndParityFromString(&xParity, &yParity, regionItem->data.V);
+              psListAdd(biassecs, PS_LIST_TAIL, region);
+              psFree(region);           // Drop reference
+          }
+          psFree(regionsIter);
+          break;
+      }
+      default:
+        psError(PS_ERR_UNKNOWN, true, "CELL.BIASSEC after read is not of type STRING or LIST --- assuming "
+                "blank.\n");
+    }
+
+    // Need to correct for binning when CELL.BIASSEC are specified immutably in the CELLS
+    if (source == PM_CONCEPT_SOURCE_CELLS) {
+        psMetadataAddBool(cell->concepts, PS_LIST_TAIL, "CELL.BIASSEC.UPDATE", 0,
+                          "Need to update CELL.BIASSEC when the binning is available.",
+                          true);
+    }
+
+    psMetadataItem *item = psMetadataItemAllocPtr(pattern->name, PS_DATA_LIST, pattern->comment, biassecs);
+    psFree(biassecs);               // Drop reference
+    return item;
+}
+
+// CELL.XBIN and CELL.YBIN
+psMetadataItem *p_pmConceptParse_CELL_Binning(const psMetadataItem *concept,
+                                              const psMetadataItem *pattern,
+                                              pmConceptSource source,
+                                              const psMetadata *cameraFormat,
+                                              const pmFPA *fpa,
+                                              const pmChip *chip,
+                                              const pmCell *cell)
+{
+    assert(concept);
+    assert(pattern);
+
+    int binning = 1;                    // Binning factor in x
+    switch (concept->type) {
+      case PS_DATA_STRING: {
+          psString binString = concept->data.V; // The string containing the binning
+          if ((strcmp(pattern->name, "CELL.XBIN") == 0 && sscanf(binString, "%d %*d", &binning) != 1 &&
+               sscanf(binString, "%d,%*d", &binning) != 1) ||
+              (strcmp(pattern->name, "CELL.YBIN") == 0 && sscanf(binString, "%*d %d", &binning) != 1 &&
+               sscanf(binString, "%*d,%d", &binning) != 1)) {
+              psError(PS_ERR_UNKNOWN, true, "Unable to parse string to get %s: %s\n", pattern->name, binString);
+          }
+          break;
+      }
+        TYPE_CASE(binning, concept, U8);
+        TYPE_CASE(binning, concept, U16);
+        TYPE_CASE(binning, concept, U32);
+        TYPE_CASE(binning, concept, S8);
+        TYPE_CASE(binning, concept, S16);
+        TYPE_CASE(binning, concept, S32);
+      default:
+        psError(PS_ERR_UNKNOWN, true, "Note sure how to parse %s of type %x --- assuming 1.\n", pattern->name,
+                concept->type);
+    }
+
+    return psMetadataItemAllocS32(pattern->name, pattern->comment, binning);
+}
+
+
+psMetadataItem *p_pmConceptParse_TIMESYS(const psMetadataItem *concept,
+                                         const psMetadataItem *pattern,
+                                         pmConceptSource source,
+                                         const psMetadata *cameraFormat,
+                                         const pmFPA *fpa,
+                                         const pmChip *chip,
+                                         const pmCell *cell)
+{
+    assert(concept);
+    assert(pattern);
+
+    psTimeType timeSys = PS_TIME_UTC;   // The time system
+    psString sys = concept->data.V;     // The time system string
+    if (concept->type != PS_DATA_STRING || strlen(sys) <= 0) {
+        psError(PS_ERR_UNKNOWN, true, "Can't interpret %s --- assuming UTC.\n", pattern->name);
+    } else if (strcasecmp(sys, "TAI") == 0) {
+        timeSys = PS_TIME_TAI;
+    } else if (strcasecmp(sys, "UTC") == 0) {
+        timeSys = PS_TIME_UTC;
+    } else if (strcasecmp(sys, "UT1") == 0) {
+        timeSys = PS_TIME_UT1;
+    } else if (strcasecmp(sys, "TT") == 0) {
+        timeSys = PS_TIME_TT;
+    } else {
+        psError(PS_ERR_UNKNOWN, true, "Can't interpret %s --- assuming UTC.\n", pattern->name);
+    }
+
+    return psMetadataItemAllocS32(pattern->name, pattern->comment, timeSys);
+}
+
+psMetadataItem *p_pmConceptParse_TIME(const psMetadataItem *concept,
+                                      const psMetadataItem *pattern,
+                                      pmConceptSource source,
+                                      const psMetadata *cameraFormat,
+                                      const pmFPA *fpa,
+                                      const pmChip *chip,
+                                      const pmCell *cell)
+{
+    assert(concept);
+    assert(cameraFormat);
+
+    // Need TIMESYS first
+    psString timesysName = psStringCopy(pattern->name); // e.g., "CELL.TIME" --> "CELL.TIMESYS"
+    psStringSubstitute(&timesysName, "TIMESYS", "TIME");
+    bool mdok = false;                  // Result of MD lookup
+    psTimeType timeSys = PS_TIME_UTC; // The time system
+    if (cell) {
+        timeSys = psMetadataLookupS32(&mdok, cell->concepts, timesysName);
+    }
+    if (!mdok && chip) {
+        timeSys = psMetadataLookupS32(&mdok, chip->concepts, timesysName);
+    }
+    if (!mdok && fpa) {
+        timeSys = psMetadataLookupS32(&mdok, fpa->concepts, timesysName);
+    }
+    if (!mdok || (timeSys == 0xffffffff)) {
+        psWarning("Unable to find %s in concepts when parsing %s --- assuming UTC.\n",
+                  timesysName, pattern->name);
+        timeSys = PS_TIME_UTC;
+    }
+    psFree(timesysName);
+
+    // Work out how the time is represented
+    bool separateTime = false;
+    bool usaTime = false;               // Is the time specified in USA (MM-DD-YYYY) order?
+    bool backwardsTime = false;         // Is the time specified in backwards (DD-MM-YYYY) order?
+    bool yearFirst = false;            // Is the time specified in yearFirst (YYYY-MM-DD) order?
+    bool pre2000Time = false;           // Is the time specified pre-2000 (where the year only has two digits)
+    bool mjdTime = false;               // Is the time specified a MJD?
+    bool jdTime = false;                // Is the time specified a JD?
+
+    // Get format
+    psMetadata *formats = psMetadataLookupMetadata(&mdok, cameraFormat, "FORMATS");
+    if (!mdok || !formats) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find FORMATS in camera configuration.\n");
+        return NULL;
+    }
+
+    psString timeFormat = psMetadataLookupStr(&mdok, formats, pattern->name);
+    if (!mdok || strlen(timeFormat) == 0) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find %s in FORMATS.\n", pattern->name);
+        return NULL;
+    }
+
+    // Parse the time format
+    // why should more than one be allowed??
+    psList *timeFormats = psStringSplit(timeFormat, " ,;", false); // List of the format options
+    psListIterator *timeFormatsIter = psListIteratorAlloc(timeFormats, PS_LIST_HEAD, false); // Iter
+    while ((timeFormat = psListGetAndIncrement(timeFormatsIter))) {
+        if (strcasecmp(timeFormat, "SEPARATE") == 0) {
+            separateTime = true;
+        } else if (strcasecmp(timeFormat, "USA") == 0) {
+            usaTime = true;
+            backwardsTime = false;
+            yearFirst = false;
+            jdTime = false;
+            mjdTime = false;
+        } else if (strcasecmp(timeFormat, "BACKWARDS") == 0) {
+            backwardsTime = true;
+            usaTime = false;
+            yearFirst = false;
+            jdTime = false;
+            mjdTime = false;
+        } else if (strcasecmp(timeFormat, "YEAR.FIRST") == 0) {
+            yearFirst = true;
+            usaTime = false;
+            backwardsTime = false;
+            jdTime = false;
+            mjdTime = false;
+        } else if (strcasecmp(timeFormat, "PRE2000") == 0) {
+            pre2000Time = true;
+        } else if (strcasecmp(timeFormat, "MJD") == 0) {
+            mjdTime = true;
+            jdTime = false;
+            yearFirst = false;
+            backwardsTime = false;
+            usaTime = false;
+        } else if (strcasecmp(timeFormat, "JD") == 0) {
+            jdTime = true;
+            mjdTime = false;
+            yearFirst = false;
+            backwardsTime = false;
+            usaTime = false;
+        } else {
+            psError(PS_ERR_UNKNOWN, true, "Unrecognised FORMATS option for %s: %s --- "
+                    "ignored.\n", pattern->name, timeFormat);
+            psFree(timeFormatsIter);
+            psFree(timeFormats);
+            return NULL;
+        }
+    }
+    psFree(timeFormatsIter);
+    psFree(timeFormats);
+
+    psTime *time = NULL;                // The time
+    switch (concept->type) {
+      case PS_DATA_LIST: {
+          if (!separateTime) {
+              psWarning ("DATE and TIME stored separately, but not specified in format\n");
+          }
+          // The date and time are stored separately
+          // Assume the date is first and the time second
+          psList *dateTime = concept->data.V; // The list containing items for date and time
+          if (psListLength(dateTime) != 2) {
+              psError(PS_ERR_BAD_PARAMETER_SIZE, false,
+                      "Unable to parse %s: date and time are not both available.", pattern->name);
+              return NULL;
+          }
+          psMetadataItem *dateItem = psListGet(dateTime, PS_LIST_HEAD); // Item containing the date
+          if (!dateItem) {
+              psError(PS_ERR_UNKNOWN, true, "Date is not found.\n");
+              return NULL;
+          }
+          if (dateItem->type != PS_DATA_STRING) {
+              psError(PS_ERR_UNKNOWN, true, "Date is not of type STR.\n");
+              return NULL;
+          }
+          psString dateString = dateItem->data.V; // The string with the date
+          int day = 0, month = 0, year = 0;
+          if (sscanf(dateString, "%d-%d-%d", &year, &month, &day) != 3 &&
+              sscanf(dateString, "%d/%d/%d", &year, &month, &day) != 3) {
+              psError(PS_ERR_UNKNOWN, true, "Unable to read date: %s\n", dateString);
+              return NULL;
+          }
+          if (backwardsTime) {
+              // Need to switch days and years
+              int temp = day;
+              day = year;
+              year = temp;
+          }
+          if (usaTime) {
+              // Need to switch everything around.... Yanks!
+              int temp = day;
+              day = month;
+              month = year;
+              year = temp;
+          }
+          if (year < 100) {
+              if (pre2000Time) {
+                  year += 1900;
+              } else {
+                  year += 2000;
+              }
+          }
+          sprintf(dateString,"%04d-%02d-%02d", year, month, day);
+
+          psMetadataItem *timeItem = psListGet(dateTime, PS_LIST_HEAD + 1); // Item containing the time
+          if (!timeItem) {
+              psError(PS_ERR_UNKNOWN, true, "Time is not found.\n");
+              return NULL;
+          }
+          psString timeString = NULL; // The string with the time
+          if (timeItem->type == PS_DATA_STRING) {
+              timeString = timeItem->data.V;
+          } else {
+              // Assume that time is specified in Second of Day (!)
+              double seconds = NAN;
+              switch (timeItem->type) {
+                  TYPE_CASE(seconds, timeItem, U8);
+                  TYPE_CASE(seconds, timeItem, U16);
+                  TYPE_CASE(seconds, timeItem, U32);
+                  TYPE_CASE(seconds, timeItem, S8);
+                  TYPE_CASE(seconds, timeItem, S16);
+                  TYPE_CASE(seconds, timeItem, S32);
+                  TYPE_CASE(seconds, timeItem, F32);
+                  TYPE_CASE(seconds, timeItem, F64);
+                default:
+                  psError(PS_ERR_UNKNOWN, true, "Time is not of an expected type: %x\n", timeItem->type);
+                  return NULL;
+              }
+              // Now print to timeString as "hh:mm:ss.ss"
+              int hours = seconds / 3600;
+              seconds -= (double)hours * 3600.0;
+              int minutes = seconds / 60;
+              seconds -= (double)minutes * 60.0;
+              psStringAppend(&timeString, "%02d:%02d:%02f", hours, minutes, seconds);
+          }
+          psString dateTimeString = NULL;
+          psStringAppend(&dateTimeString, "%sT%s", dateString, timeString);
+          time = psTimeFromISO(dateTimeString, timeSys);
+          psFree(dateTimeString);
+          break;
+      }
+      case PS_DATA_STRING: {
+          psString timeString = concept->data.V;   // String with the time
+          if (jdTime) {
+              double timeValue = strtod (timeString, NULL);
+              time = psTimeFromJD(timeValue);
+          } else if (mjdTime) {
+              double timeValue = strtod (timeString, NULL);
+              time = psTimeFromMJD(timeValue);
+          } else {
+              // It's ISO
+              time = psTimeFromISO(timeString, timeSys);
+          } // Interpreting the time string
+          break;
+      }
+      case PS_TYPE_F32: {
+          double timeValue = (double)concept->data.F32;
+          if (jdTime) {
+              time = psTimeFromJD(timeValue);
+          } else if (mjdTime) {
+              time = psTimeFromMJD(timeValue);
+          } else {
+              psError(PS_ERR_UNKNOWN, true, "Not sure how to parse %s (%f) --- trying JD\n",
+                      pattern->name, timeValue);
+              time = psTimeFromJD(timeValue);
+          }
+          break;
+      }
+      case PS_TYPE_F64: {
+          double timeValue = (double)concept->data.F64;
+          if (jdTime) {
+              time = psTimeFromJD(timeValue);
+          } else if (mjdTime) {
+              time = psTimeFromMJD(timeValue);
+          } else {
+              psError(PS_ERR_UNKNOWN, true, "Not sure how to parse %s (%f) --- trying JD\n",
+                      pattern->name, timeValue);
+              time = psTimeFromJD(timeValue);
+          }
+          break;
+      }
+      default:
+        psError(PS_ERR_UNKNOWN, true, "Unable to parse %s.\n", pattern->name);
+        return NULL;
+    }
+
+    psMetadataItem *item = psMetadataItemAllocPtr(pattern->name, PS_DATA_TIME, pattern->comment, time);
+    psFree(time);                       // Drop reference
+    return item;
+}
+
+// Correct a position --- in case the user wants FORTRAN indexing (like the FITS standard...)
+static int fortranCorr(const psMetadata *cameraFormat, // The camera format description
+                       const char *name // Name of concept to check for FORTRAN indexing
+    )
+{
+    bool mdok = false;                  // Result of MD lookup
+    psMetadata *formats = psMetadataLookupMetadata(&mdok, cameraFormat, "FORMATS");
+    if (mdok && formats) {
+        psString format = psMetadataLookupStr(&mdok, formats, name);
+        if (mdok && strlen(format) > 0 && strcasecmp(format, "FORTRAN") == 0) {
+            return 1;
+        }
+    }
+    return 0;
+}
+
+psMetadataItem *p_pmConceptParse_Positions(const psMetadataItem *concept,
+                                           const psMetadataItem *pattern,
+                                           pmConceptSource source,
+                                           const psMetadata *cameraFormat,
+                                           const pmFPA *fpa,
+                                           const pmChip *chip,
+                                           const pmCell *cell)
+{
+    assert(concept);
+    assert(cameraFormat);
+
+    int offset = 0;                     // Offset of component (0,0) corner from the parent (0,0) corner
+
+    switch (concept->type) {
+        TYPE_CASE(offset, concept, U8);
+        TYPE_CASE(offset, concept, U16);
+        TYPE_CASE(offset, concept, U32);
+        TYPE_CASE(offset, concept, S8);
+        TYPE_CASE(offset, concept, S16);
+        TYPE_CASE(offset, concept, S32);
+#if 0
+
+      case PS_DATA_STRING: {
+          // Interpret as a region specifier [x0:x1,y0:y1]
+          int xParity = 0;
+          int yParity = 0;
+          psRegion region = psRegionAndParityFromString(&xParity, &yParity, concept->data.V);
+          if (strstr(pattern->name, ".X0")) {
+              offset = region.x0;
+          } else if (strstr(pattern->name, ".Y0")) {
+              offset = region.y0;
+          } else if (strstr(pattern->name, ".X1")) {
+              offset = region.x1;
+          } else if (strstr(pattern->name, ".Y1")) {
+              offset = region.y1;
+          } else {
+              psError(PS_ERR_UNKNOWN, true,
+                      "Unable to interpret %s because unable to determine if concept is X or Y.\n",
+                      pattern->name);
+              return NULL;
+          }
+          break;
+      }
+#endif
+      default:
+        if (concept->type == PS_TYPE_F32 && concept->data.F32 - (int)concept->data.F32 == 0) {
+            offset = concept->data.F32;
+        } else if (concept->type == PS_TYPE_F64 && concept->data.F64 - (int)concept->data.F64 == 0) {
+            offset = concept->data.F64;
+        } else {
+            psError(PS_ERR_UNKNOWN, true, "Concept %s is not of integer type, as expected.\n", pattern->name);
+            return NULL;
+        }
+    }
+    offset -= fortranCorr(cameraFormat, pattern->name);
+    return psMetadataItemAllocS32(pattern->name, pattern->comment, offset);
+}
+
+
+psMetadataItem *p_pmConceptFormat_CELL_TRIMSEC(const psMetadataItem *concept,
+                                               pmConceptSource source,
+                                               const psMetadata *cameraFormat,
+                                               const pmFPA *fpa,
+                                               const pmChip *chip,
+                                               const pmCell *cell)
+{
+    assert(concept);
+
+    psRegion *trimsec = psMemIncrRefCounter(concept->data.V); // The trimsec region
+    if (!trimsec) {
+        return psMetadataItemAllocStr(concept->name, concept->comment, NULL);
+    }
+
+    // Correct trim section for binning if it's specified explicitly (i.e., immutably) in the CELLS.
+    if (source == PM_CONCEPT_SOURCE_CELLS) {
+        bool xStatus, yStatus;          // Status of MD lookups
+        int xBin = psMetadataLookupS32(&xStatus, cell->concepts, "CELL.XBIN");
+        int yBin = psMetadataLookupS32(&yStatus, cell->concepts, "CELL.YBIN");
+        if (!xStatus || !yStatus || xBin == 0 || yBin == 0) {
+            psWarning("Unable to find CELL.XBIN and CELL.YBIN to correct CELL.TRIMSEC.\n");
+            psFree(trimsec);            // Drop reference
+            return psMemIncrRefCounter((psPtr)concept); // Casting away "const" to increment
+        }
+        psRegion *newTrimsec = psRegionAlloc(trimsec->x0 * xBin, trimsec->x1 * xBin,
+                                             trimsec->y0 * yBin, trimsec->y1 * yBin); // Adjusted for binning
+        psFree(trimsec);
+        trimsec = newTrimsec;
+    }
+
+    psString trimsecString = psRegionToString(*trimsec);
+    psFree(trimsec);
+    psMetadataItem *formatted = psMetadataItemAllocStr(concept->name, concept->comment,
+                                                       trimsecString);
+    psFree(trimsecString);
+    return formatted;
+}
+
+psMetadataItem *p_pmConceptFormat_CELL_BIASSEC(const psMetadataItem *concept,
+                                               pmConceptSource source,
+                                               const psMetadata *cameraFormat,
+                                               const pmFPA *fpa,
+                                               const pmChip *chip,
+                                               const pmCell *cell)
+{
+    // Return a metadata item containing a list of metadata items of region strings
+    psList *biassecs = concept->data.V; // The biassecs region list
+    psList *new = psListAlloc(NULL);    // New list containing metadatas
+    if (biassecs) {
+        psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false); // Iterator
+        psRegion *region = NULL;            // Region from iteration
+        while ((region = psListGetAndIncrement(biassecsIter))) {
+            // Correct bias section for binning if it's specified explicitly (i.e., immutably) in the CELLS.
+            if (source == PM_CONCEPT_SOURCE_CELLS) {
+                bool xStatus, yStatus;          // Status of MD lookups
+                int xBin = psMetadataLookupS32(&xStatus, cell->concepts, "CELL.XBIN");
+                int yBin = psMetadataLookupS32(&yStatus, cell->concepts, "CELL.YBIN");
+                if (!xStatus || !yStatus || xBin == 0 || yBin == 0) {
+                    psWarning("Unable to find CELL.XBIN and CELL.YBIN to correct CELL.BIASSEC.\n");
+                } else {
+                    psRegion *newTrimsec = psRegionAlloc(region->x0 * xBin, region->x1 * xBin,
+                                                         region->y0 * yBin, region->y1 * yBin);
+                    region = newTrimsec;
+                }
+            } else {
+                psMemIncrRefCounter(region);
+            }
+
+            psString regionString = psRegionToString(*region); // The string region "[x0:x1,y0:y1]"
+            psFree(region);
+            psMetadataItem *item = psMetadataItemAllocStr(concept->name, concept->comment, regionString);
+            psFree(regionString);           // Drop reference
+            psListAdd(new, PS_LIST_TAIL, item);
+            psFree(item);                   // Drop reference
+        }
+        psFree(biassecsIter);
+    }
+
+    psMetadataItem *formatted = psMetadataItemAllocPtr(concept->name, PS_DATA_LIST, concept->comment, new);
+    psFree(new);                        // Drop reference
+    return formatted;
+}
+
+// This function actually does both CELL.XBIN and CELL.YBIN if CELL.XBIN and CELL.YBIN are specified by the
+// same header.
+psMetadataItem *p_pmConceptFormat_CELL_XBIN(const psMetadataItem *concept,
+                                            pmConceptSource source,
+                                            const psMetadata *cameraFormat,
+                                            const pmFPA *fpa,
+                                            const pmChip *chip,
+                                            const pmCell *cell)
+{
+    assert(concept);
+
+    psMetadata *translation = psMetadataLookupMetadata(NULL, cameraFormat, "TRANSLATION");
+    bool xBinOK = true, yBinOK = true;  // Status of MD lookups
+    psString xKeyword = psMetadataLookupStr(&xBinOK, translation, "CELL.XBIN");
+    psString yKeyword = psMetadataLookupStr(&yBinOK, translation, "CELL.YBIN");
+    if (xBinOK && yBinOK && strlen(xKeyword) > 0 && strlen(yKeyword) > 0 &&
+        strcasecmp(xKeyword, yKeyword) == 0) {
+        psMetadataItem *yBinItem = psMetadataLookup(cell->concepts, "CELL.YBIN"); // Binning factor in y
+        psString binString = NULL;
+        psStringAppend(&binString, "%d %d", concept->data.S32, yBinItem->data.S32);
+        psMetadataItem *binItem = psMetadataItemAllocStr(concept->name, concept->comment, binString);
+        psFree(binString);
+        return binItem;
+    }
+
+    // Otherwise, there's no formatting required
+    return psMetadataItemCopy(concept);
+}
+
+// Only need to format if both if CELL.XBIN and CELL.YBIN are not specified by the same header.
+psMetadataItem *p_pmConceptFormat_CELL_YBIN(const psMetadataItem *concept,
+                                            pmConceptSource source,
+                                            const psMetadata *cameraFormat,
+                                            const pmFPA *fpa,
+                                            const pmChip *chip,
+                                            const pmCell *cell)
+{
+    assert(concept);
+
+    psMetadata *translation = psMetadataLookupMetadata(NULL, cameraFormat, "TRANSLATION");
+    bool xBinOK = true, yBinOK = true;  // Status of MD lookups
+    psString xKeyword = psMetadataLookupStr(&xBinOK, translation, "CELL.XBIN");
+    psString yKeyword = psMetadataLookupStr(&yBinOK, translation, "CELL.YBIN");
+    if (xBinOK && yBinOK && strlen(xKeyword) > 0 && strlen(yKeyword) > 0 &&
+        strcasecmp(xKeyword, yKeyword) == 0) {
+        // Censor this --- it's already done (though no harm if it's done twice
+        return NULL;
+    }
+
+    // No formatting required
+    return psMetadataItemCopy(concept);
+}
+
+
+psMetadataItem *p_pmConceptFormat_TIMESYS(const psMetadataItem *concept,
+                                          pmConceptSource source,
+                                          const psMetadata *cameraFormat,
+                                          const pmFPA *fpa,
+                                          const pmChip *chip,
+                                          const pmCell *cell)
+{
+    psString sys = NULL;            // String to store
+    switch (concept->data.S32) {
+      case PS_TIME_TAI:
+        sys = psStringCopy("TAI");
+        break;
+      case PS_TIME_UTC:
+        sys = psStringCopy("UTC");
+        break;
+      case PS_TIME_UT1:
+        sys = psStringCopy("UT1");
+        break;
+      case PS_TIME_TT:
+        sys = psStringCopy("TT");
+        break;
+      default:
+        sys = psStringCopy("Unknown");
+    }
+    psMetadataItem *newItem = psMetadataItemAllocStr(concept->name, concept->comment, sys);
+    psFree(sys);
+
+    return newItem;
+}
+
+psMetadataItem *p_pmConceptFormat_TIME(const psMetadataItem *concept,
+                                       pmConceptSource source,
+                                       const psMetadata *cameraFormat,
+                                       const pmFPA *fpa,
+                                       const pmChip *chip,
+                                       const pmCell *cell)
+{
+    psTime *time = concept->data.V;     // The time
+
+    // Work out the format
+    bool separateTime = false;          // Are the date and time stored separately?
+    bool pre2000Time = false;           // Is the year in pre-2000 format (two digits only)?
+    bool backwardsTime = false;         // Is the date stored backwards (DD-MM-YYYY)?
+    bool usaTime = false;               // Is the date stored in USA order (MM-DD-YYYY)?
+    bool jdTime = false;                // Is the date stored as a JD?
+    bool mjdTime = false;               // Is the date stored as a MJD?
+    bool yearFirst = false;
+
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *formats = psMetadataLookupMetadata(&mdok, cameraFormat, "FORMATS"); // The formats
+    if (mdok && formats) {
+        psString format = psMetadataLookupStr(&mdok, formats, concept->name); // The formats for eg, CELL.TIME
+        if (mdok && format && strlen(format) > 0) {
+            psList *formatList = psStringSplit(format, " ,;", false); // List of formats specified
+            psListIterator *formatListIter = psListIteratorAlloc(formatList, PS_LIST_HEAD, false); // Iterator
+            while ((format = psListGetAndIncrement(formatListIter))) {
+                if (strcasecmp(format, "SEPARATE") == 0) {
+                    separateTime = true;
+                } else if (strcasecmp(format, "USA") == 0) {
+                    usaTime = true;
+                    backwardsTime = false;
+                    jdTime = false;
+                    mjdTime = false;
+                } else if (strcasecmp(format, "BACKWARDS") == 0) {
+                    backwardsTime = true;
+                    usaTime = false;
+                    mjdTime = false;
+                    jdTime = false;
+                } else if (strcasecmp(format, "YEAR.FIRST") == 0) {
+                    yearFirst = true;
+                    usaTime = false;
+                    backwardsTime = false;
+                    jdTime = false;
+                    mjdTime = false;
+                } else if (strcasecmp(format, "PRE2000") == 0) {
+                    pre2000Time = true;
+                } else if (strcasecmp(format, "MJD") == 0) {
+                    mjdTime = true;
+                    usaTime = false;
+                    backwardsTime = false;
+                    jdTime = false;
+                    separateTime = false;
+                } else if (strcasecmp(format, "JD") == 0) {
+                    jdTime = true;
+                    usaTime = false;
+                    backwardsTime = false;
+                    mjdTime = false;
+                    separateTime = false;
+                } else {
+                    psWarning("Unrecognised FORMATS option for %s: %s --- "
+                              "ignored.\n", concept->name, format);
+                }
+            }
+            psFree(formatListIter);
+            psFree(formatList);
+        }
+    }
+
+    if (separateTime) {
+        // We're working with two separate headers --- construct a list with the date and time separately
+        psString dateTimeString = psTimeToISO(time); // String representation
+        psList *dateTime = psStringSplit(dateTimeString, "T", true);
+        psFree(dateTimeString);
+        psString dateString = psListGet(dateTime, PS_LIST_HEAD); // The date string
+        psString timeString = psListGet(dateTime, PS_LIST_TAIL); // The time string
+
+        // Need to format the strings....
+        // XXX: Couldn't be bothered doing these right now
+        if (pre2000Time) {
+            psError(PS_ERR_UNKNOWN, true, "Don't you realise it's the twenty-first century?\n");
+            return NULL;
+        }
+        if (backwardsTime) {
+            int day, month, year;
+            psTrace ("psModules.concepts", 5, "ISO time has year first, convert to DD-MM-YYYY");
+            sscanf (dateString, "%d-%d-%d", &year, &month, &day);
+            sprintf (dateString, "%02d-%02d-%04d", day, month, year);
+            // XXX fix this for str length
+        }
+        if (usaTime) {
+            int day, month, year;
+            psTrace ("psModules.concepts", 5, "ISO time has year first, convert to MM-DD-YYYY");
+            sscanf (dateString, "%d-%d-%d", &year, &month, &day);
+            sprintf (dateString, "%02d-%02d-%04d", month, day, year);
+            // XXX fix this for str length
+        }
+        if (yearFirst) {
+            psTrace ("psModules.concepts", 5, "ISO time has year first, no adjustment needed");
+        }
+
+        psMetadataItem *dateItem = psMetadataItemAllocStr(concept->name, "The date of observation",
+                                                          dateString);
+        psMetadataItem *timeItem = psMetadataItemAllocStr(concept->name, "The time of observation",
+                                                          timeString);
+
+        psListRemove(dateTime, PS_LIST_HEAD);
+        psListRemove(dateTime, PS_LIST_HEAD);
+
+        psListAdd(dateTime, PS_LIST_HEAD, dateItem);
+        psListAdd(dateTime, PS_LIST_TAIL, timeItem);
+
+        psMetadataItem *item = psMetadataItemAllocPtr(concept->name, PS_DATA_LIST, concept->comment,
+                                                      dateTime);
+        psFree (dateItem);
+        psFree (timeItem);
+        psFree (dateTime);
+        return item;
+    }
+    if (jdTime) {
+        double jd = psTimeToMJD(time);
+        return psMetadataItemAllocF64(concept->name, concept->comment, jd);
+    }
+    if (mjdTime) {
+        double mjd = psTimeToMJD(time);
+        return psMetadataItemAllocF64(concept->name, concept->comment, mjd);
+    }
+
+    // If we've gotten this far, so it's straight ISO.
+    psString dateTimeString = psTimeToISO(time); // String representation
+    psMetadataItem *item = psMetadataItemAllocStr(concept->name, concept->comment, dateTimeString);
+    psFree(dateTimeString);
+    return item;
+}
+
+
+psMetadataItem *p_pmConceptFormat_Positions(const psMetadataItem *concept,
+                                            pmConceptSource source,
+                                            const psMetadata *cameraFormat,
+                                            const pmFPA *fpa,
+                                            const pmChip *chip,
+                                            const pmCell *cell)
+{
+    assert(concept);
+    assert(cameraFormat);
+
+    if (concept->type != PS_TYPE_S32) {
+        psError(PS_ERR_UNKNOWN, true, "Concept %s is not of type S32, as expected.\n", concept->name);
+        return NULL;
+    }
+
+#if 0
+    // If both the X0 and Y0 positions are specified by the same header keyword, write both together, as part
+    // of the call for the X0 position.
+    // This is a bit of a kludge --- we're going to write it out as [x0:0,y0:0].  This means that we will be
+    // able to read it back in, but we've destroyed the x1 and y1 if it was present.
+    // We *could* attempt to read the header, parse the region, and only update the ones that we're trying
+    // to update.  Consider this an upgrade option later.
+    // Alternatively, we could add X1 and Y1 concepts, and write the whole lot out together.
+    // But until we care about X1 and Y1, it doesn't really matter --- if you want X1 and Y1, look at X0 and
+    // Y0 and add NXAIS1 and NAXIS2, respectively....
+    if (strstr(concept->name, ".X0")) {
+        psString companion = psStringCopy(concept->name); // Companion entry: ".Y" where this one has ".X"
+        psStringSubstitute(&companion, ".Y0", ".X0");
+
+        // Look both up in the camera format config
+        psMetadata *translation = psMetadataLookupMetadata(NULL, cameraFormat, "TRANSLATION");
+        bool xFound = true, yFound = true;  // Status of MD lookups
+        psString xKeyword = psMetadataLookupStr(&xFound, translation, concept->name);
+        psString yKeyword = psMetadataLookupStr(&yFound, translation, companion);
+        if (xFound && yFound && strlen(xKeyword) > 0 && strlen(yKeyword) > 0 &&
+            strcasecmp(xKeyword, yKeyword) == 0) {
+            psMetadataItem *yItem = psMetadataLookup(cell->concepts, companion); // Corresponding y value
+
+            int x = concept->data.S32 + fortranCorr(cameraFormat, concept->name); // x value
+            int y = yItem->data.S32 + fortranCorr(cameraFormat, companion); // y value
+
+            psRegion region = psRegionSet(x, x, y, y);
+            psString string = psRegionToString(region);
+            psMetadataItem *binItem = psMetadataItemAllocStr(concept->name, concept->comment, string);
+            psFree(string);
+            psFree(companion);
+            return binItem;
+        }
+        psFree(companion);
+    } else if (strstr(concept->name, ".Y0")) {
+        psString companion = psStringCopy(concept->name); // Companion entry: ".Y" where this one has ".X"
+        psStringSubstitute(&companion, ".X0", ".Y0");
+
+        // Look both up in the camera format config
+        psMetadata *translation = psMetadataLookupMetadata(NULL, cameraFormat, "TRANSLATION");
+        bool xFound = true, yFound = true;  // Status of MD lookups
+        psString xKeyword = psMetadataLookupStr(&xFound, translation, concept->name);
+        psString yKeyword = psMetadataLookupStr(&yFound, translation, companion);
+        psFree(companion);
+        if (xFound && yFound && strlen(xKeyword) > 0 && strlen(yKeyword) > 0 &&
+            strcasecmp(xKeyword, yKeyword) == 0) {
+            return NULL;                // We did it with the X; don't do anything.
+        }
+    }
+#endif
+
+    int offset = concept->data.S32;
+    offset += fortranCorr(cameraFormat, concept->name);
+    return psMetadataItemAllocS32(concept->name, concept->comment, offset);
+}
+
+
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsStandard.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsStandard.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsStandard.h	(revision 15358)
@@ -0,0 +1,182 @@
+/* @file  pmConceptsStandard.h
+ * @brief Private functions for parsing and formatting standard concepts.
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.11 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-10-12 03:18:11 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONCEPTS_STANDARD_H
+#define PM_CONCEPTS_STANDARD_H
+
+/// @addtogroup Concepts Data Abstraction Concepts
+/// @{
+
+/// Parse a list of region specifiers on a line
+psList *p_pmConceptParseRegions(const char *region ///< Regions, separated by whitespace
+    );
+
+/// Parse the FPA.FILTER concept to apply a lookup table
+psMetadataItem *p_pmConceptParse_FPA_FILTER(const psMetadataItem *concept, ///< Concept to parse
+        const psMetadataItem *pattern, ///< Pattern to use in parsing
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                           );
+
+/// Format the FPA.FILTER concept to (reverse-)apply a lookup table
+psMetadataItem *p_pmConceptFormat_FPA_FILTER(const psMetadataItem *concept, ///< Concept to format
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                            );
+
+/// Parse the coordinates concepts: FPA.RA and FPA.DEC
+psMetadataItem *p_pmConceptParse_FPA_Coords(const psMetadataItem *concept, ///< Concept to parse
+        const psMetadataItem *pattern, ///< Pattern to use in parsing
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                           );
+
+/// Format the coordinates concepts: FPA.RA and FPA.DEC
+psMetadataItem *p_pmConceptFormat_FPA_Coords(const psMetadataItem *concept, ///< Concept to format
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                            );
+
+/// Parse the CELL.TRIMSEC concept
+psMetadataItem *p_pmConceptParse_CELL_TRIMSEC(const psMetadataItem *concept, ///< Concept to parse
+        const psMetadataItem *pattern, ///< Pattern to use in parsing
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                             );
+
+/// Parse the CELL.BIASSEC concept
+psMetadataItem *p_pmConceptParse_CELL_BIASSEC(const psMetadataItem *concept, ///< Concept to parse
+        const psMetadataItem *pattern, ///< Pattern to use in parsing
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                             );
+
+/// Parse the cell binning concepts: CELL.XBIN, CELL.YBIN
+psMetadataItem *p_pmConceptParse_CELL_Binning(const psMetadataItem *concept, ///< Concept to parse
+        const psMetadataItem *pattern, ///< Pattern to use in parsing
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                             );
+
+/// Parse the time system concepts: FPA.TIMESYS and CELL.TIMESYS
+psMetadataItem *p_pmConceptParse_TIMESYS(const psMetadataItem *concept, ///< Concept to parse
+        const psMetadataItem *pattern, ///< Pattern to use in parsing
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                        );
+
+/// Parse the time concepts: FPA.TIME and CELL.TIME
+psMetadataItem *p_pmConceptParse_TIME(const psMetadataItem *concept, ///< Concept to parse
+                                      const psMetadataItem *pattern, ///< Pattern to use in parsing
+                                            pmConceptSource source, ///< Source for concept
+                                      const psMetadata *cameraFormat, ///< Camera format definition
+                                      const pmFPA *fpa, ///< FPA for concept, or NULL
+                                      const pmChip *chip, ///< Chip for concept, or NULL
+                                      const pmCell *cell ///< Cell for concept, or NULL
+                                     );
+
+/// Parse a cell position concept, e.g., CELL.X0
+psMetadataItem *p_pmConceptParse_Positions(const psMetadataItem *concept, ///< Concept to parse
+        const psMetadataItem *pattern, ///< Pattern to use in parsing
+                                            pmConceptSource source, ///< Source for concept
+       const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                          );
+
+/// Format the CELL.TRIMSEC concept
+psMetadataItem *p_pmConceptFormat_CELL_TRIMSEC(const psMetadataItem *concept, ///< Concept to format
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                              );
+
+/// Format the CELL.BIASSEC concept
+psMetadataItem *p_pmConceptFormat_CELL_BIASSEC(const psMetadataItem *concept, ///< Concept to format
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                              );
+
+/// Format the CELL.XBIN concept
+psMetadataItem *p_pmConceptFormat_CELL_XBIN(const psMetadataItem *concept, ///< Concept to format
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                           );
+
+/// Format the CELL.YBIN concept
+psMetadataItem *p_pmConceptFormat_CELL_YBIN(const psMetadataItem *concept, ///< Concept to format
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                           );
+
+/// Format the time system concepts: FPA.TIMESYS and CELL.TIMESYS
+psMetadataItem *p_pmConceptFormat_TIMESYS(const psMetadataItem *concept, ///< Concept to format
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                         );
+
+/// Format the time concepts: FPA.TIME and CELL.TIME
+psMetadataItem *p_pmConceptFormat_TIME(const psMetadataItem *concept, ///< Concept to format
+                                            pmConceptSource source, ///< Source for concept
+                                       const psMetadata *cameraFormat, ///< Camera format definition
+                                       const pmFPA *fpa, ///< FPA for concept, or NULL
+                                       const pmChip *chip, ///< Chip for concept, or NULL
+                                       const pmCell *cell ///< Cell for concept, or NULL
+                                      );
+
+/// Format a cell position concept, e.g., CELL.X0
+psMetadataItem *p_pmConceptFormat_Positions(const psMetadataItem *concept, ///< Concept to format
+                                            pmConceptSource source, ///< Source for concept
+        const psMetadata *cameraFormat, ///< Camera format definition
+        const pmFPA *fpa, ///< FPA for concept, or NULL
+        const pmChip *chip, ///< Chip for concept, or NULL
+        const pmCell *cell ///< Cell for concept, or NULL
+                                           );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsUpdate.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsUpdate.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsUpdate.c	(revision 15358)
@@ -0,0 +1,90 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmConceptsUpdate.h"
+
+bool pmConceptsUpdate(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+{
+    if (fpa) {
+        // Check for FPA concepts updates
+    }
+
+    if (chip) {
+        // Check for chip concepts updates
+    }
+
+    if (cell) {
+        // Check for cell concepts updates
+
+	bool xStatus, yStatus; // Status of MD lookups
+	psImageBinning *binning = psImageBinningAlloc();
+	binning->nXbin = psMetadataLookupS32(&xStatus, cell->concepts, "CELL.XBIN");
+	binning->nYbin = psMetadataLookupS32(&yStatus, cell->concepts, "CELL.YBIN");
+	if (!xStatus || !yStatus) {
+	    // XXX should this be an error condition?
+	    psFree (binning);
+	    return true;
+	}
+	if (!binning->nXbin || !binning->nXbin) {
+	    // XXX should this be an error condition?
+	    psFree (binning);
+	    return true;
+	}
+
+        // CELL.TRIMSEC needs to be updated for the binning
+        if (psMetadataLookup(cell->concepts, "CELL.TRIMSEC.UPDATE")) {
+	    psRegion *trimsec = psMetadataLookupPtr(NULL, cell->concepts, "CELL.TRIMSEC"); // Trim section
+	    *trimsec = psImageBinningSetRuffRegion (binning, *trimsec);
+	    // force integer pixels : truncate x0, roundup x1:
+	    trimsec->x0 = (int)trimsec->x0;
+	    if (trimsec->x1 > (int)trimsec->x1) {
+		trimsec->x1 = (int)trimsec->x1 + 1;
+	    } else {
+		trimsec->x1 = (int)trimsec->x1;
+	    }		
+	    trimsec->y0 = (int)trimsec->y0;
+	    if (trimsec->y1 > (int)trimsec->y1) {
+		trimsec->y1 = (int)trimsec->y1 + 1;
+	    } else {
+		trimsec->y1 = (int)trimsec->y1;
+	    }		
+	    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC.UPDATE");
+        }
+
+        // CELL.BIASSEC needs to be updated for the binning
+        if (psMetadataLookup(cell->concepts, "CELL.BIASSEC.UPDATE")) {
+	    psList *biassecs = psMetadataLookupPtr(NULL, cell->concepts, "CELL.BIASSEC"); // Bias sections
+	    psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, true); // Iterator
+	    psRegion *biassec; // Bias region, from iteration
+	    while ((biassec = psListGetAndIncrement(biassecsIter))) {
+		*biassec = psImageBinningSetRuffRegion (binning, *biassec);
+		// force integer pixels : truncate x0, roundup x1:
+		biassec->x0 = (int)biassec->x0;
+		if (biassec->x1 > (int)biassec->x1) {
+		    biassec->x1 = (int)biassec->x1 + 1;
+		} else {
+		    biassec->x1 = (int)biassec->x1;
+		}		
+		biassec->y0 = (int)biassec->y0;
+		if (biassec->y1 > (int)biassec->y1) {
+		    biassec->y1 = (int)biassec->y1 + 1;
+		} else {
+		    biassec->y1 = (int)biassec->y1;
+		}		
+	    }
+	    psFree(biassecsIter);
+	    psMetadataRemoveKey(cell->concepts, "CELL.BIASSEC.UPDATE");
+        }
+	psFree (binning);
+    }
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsUpdate.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsUpdate.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsUpdate.h	(revision 15358)
@@ -0,0 +1,25 @@
+/* @file  pmConceptsUpdate.h
+ * @brief Function to update concepts.
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2005-2007 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONCEPTS_UPDATE_H
+#define PM_CONCEPTS_UPDATE_H
+
+/// Check for concepts to update.
+///
+/// Updating concepts is necessary if one concept depends on the value of another.  In that case, a flag
+/// (e.g., CONCEPTNAME.UPDATE" in the concepts) can be set, and it can be updated once the required value is
+/// known.
+bool pmConceptsUpdate(const pmFPA *fpa,       ///< FPA for which to update concepts
+                      const pmChip *chip,     ///< Chip for which to update concepts
+                      const pmCell *cell      ///< Cell for which to update concepts
+    );
+
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsWrite.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsWrite.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsWrite.c	(revision 15358)
@@ -0,0 +1,572 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include <strings.h>		/* for strn?casecmp */
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmHDUUtils.h"
+#include "pmConcepts.h"
+#include "pmConceptsRead.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static bool compareConcepts(const psMetadataItem *compare, // Item to compare
+                            const psMetadataItem *standard // Standard for comparison
+                           )
+{
+    // First order checks
+    if (! compare || ! standard) {
+        return false;
+    }
+    if (strcasecmp(compare->name, standard->name) != 0) {
+        return false;
+    }
+
+    // Special case: list
+    if (compare->type == PS_DATA_LIST) {
+        // "compare" contains a list of psMetadataItems
+        // "standard" likely contains just a string (but it might possibly be a list of strings)
+        psList *cList = compare->data.V; // The list from comparison item
+        psList *sList = NULL;         // The list from standard item
+        switch (standard->type) {
+        case PS_DATA_STRING:
+            sList = psStringSplit(standard->data.V, " ;", true);
+            break;
+        case PS_DATA_LIST:
+            sList = psMemIncrRefCounter(standard->data.V);
+            break;
+        default:
+            return false;
+        }
+        if (cList->n != sList->n) {
+            psFree(sList);
+            return false;
+        }
+        psVector *match = psVectorAlloc(cList->n, PS_TYPE_U8); // Array indicating which values match
+        psVectorInit(match, 0);
+        psListIterator *cIter = psListIteratorAlloc(cList, PS_LIST_HEAD, false); // compare iterator
+        psListIterator *sIter = psListIteratorAlloc(sList, PS_LIST_HEAD, false); // standard iterator
+        psMetadataItem *cItem = NULL; // Item from compare list
+        while ((cItem = psListGetAndIncrement(cIter))) {
+            if (cItem->type != PS_DATA_STRING) {
+                psWarning("psMetadataItem from list is of type %x instead of "
+                         "%x (PS_DATA_STRING) --- can't interpret.\n", cItem->type, PS_DATA_STRING);
+                psFree(cIter);
+                psFree(sIter);
+                psFree(match);
+                psFree(sList);
+                return false;
+            }
+            psString cString = cItem->data.V; // String from compare list
+            psListIteratorSet(sIter, PS_LIST_HEAD);
+            int index = 0;            // Index for list
+            bool found = false;       // Found a match?
+            for (psString sString = NULL; (sString = psListGetAndIncrement(sIter)) && !found; index++) {
+                if (strcasecmp(cString, sString) == 0) {
+                    match->data.U8[index]++;
+                    found = true;
+                }
+            }
+            if (! found) {
+                // Can give up immediately
+                psFree(cIter);
+                psFree(sIter);
+                psFree(match);
+                psFree(sList);
+                return false;
+            }
+        }
+        // Make sure we got 100% matches in both directions
+        bool allMatch = true;         // Did all of them match?
+        for (int i = 0; i < match->n && allMatch; i++) {
+            if (!match->data.U8[i]) {
+                allMatch = false;
+            }
+        }
+        psFree(cIter);
+        psFree(sIter);
+        psFree(sList);
+        psFree(match);
+        return allMatch;
+    }
+
+    return psMetadataItemCompare(compare, standard);
+
+}
+
+
+// Format a single concept
+static psMetadataItem *conceptFormat(const pmConceptSpec *spec, // The concept specification
+                                     const psMetadataItem *concept, // The concept to parse
+                                     pmConceptSource source, // The concept source
+                                     const psMetadata *cameraFormat, // The camera format
+                                     const pmFPA *fpa, // The FPA
+                                     const pmChip *chip, // The chip
+                                     const pmCell *cell // The cell
+                                    )
+{
+    assert(spec);
+    assert(cameraFormat);
+
+    if (concept) {
+        psMetadataItem *formatted = NULL;  // The formatted concept
+        if (spec->format) {
+            formatted = spec->format(concept, source, cameraFormat, fpa, chip, cell);
+        } else if (strcmp(concept->name, spec->blank->name) != 0) {
+            // Adjust so that the name is correct
+            formatted = psMetadataItemCopy(concept);
+            psFree(formatted->name);
+            formatted->name = psStringCopy(spec->blank->name);
+        } else {
+            // Can get away with merely incrementing the reference counter
+            formatted = psMemIncrRefCounter((const psPtr)concept);
+        }
+
+        return formatted;
+    }
+    return NULL;
+}
+
+// Write a single value to a header
+static bool writeSingleHeader(pmHDU *hdu, // HDU for which to add to the header
+                              const char *keyword, // Keyword to add
+                              const psMetadataItem *item // Item to add to the header; may be NULL
+                             )
+{
+    assert(hdu);
+    assert(keyword && strlen(keyword) > 0);
+
+    if (!hdu->header) {
+        hdu->header = psMetadataAlloc();
+    }
+    if (!item) {
+        psTrace("psModules.concepts", 9, "Writing header %s: <<<BLANK>>>\n", keyword);
+        // Assume it's a NULL string: it's most easily parsed.
+        return psMetadataAddStr(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, NULL, NULL);
+    }
+    switch (item->type) {
+    case PS_DATA_STRING:
+        psTrace("psModules.concepts", 9, "Writing header %s: %s\n", keyword, item->data.str);
+        return psMetadataAddStr(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
+                                item->data.V);
+    case PS_DATA_S32:
+        psTrace("psModules.concepts", 9, "Writing header %s: %d\n", keyword, item->data.S32);
+        return psMetadataAddS32(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
+                                item->data.S32);
+    case PS_DATA_F32:
+        psTrace("psModules.concepts", 9, "Writing header %s: %f\n", keyword, item->data.F32);
+        return psMetadataAddF32(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
+                                item->data.F32);
+    case PS_DATA_F64:
+        psTrace("psModules.concepts", 9, "Writing header %s: %f\n", keyword, item->data.F64);
+        return psMetadataAddF64(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
+                                item->data.F64);
+    case PS_DATA_REGION: {
+            psString region = psRegionToString(*(psRegion*)item->data.V);
+            psTrace("psModules.concepts", 9, "Writing header %s: %s\n", keyword, region);
+            bool result = psMetadataAddStr(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
+                                           region);
+            psFree(region);
+            return result;
+        }
+    default:
+        psWarning("Type of %s is not suitable for a FITS header --- not added.\n",
+                 item->name);
+        return false;
+    }
+}
+
+
+// Write potentially multiple values to a header
+static bool writeHeader(pmHDU *hdu,     // HDU for which to add to the header
+                        const char *keywords, // Keywords to add
+                        const psMetadataItem *item // Item to add to the header
+                       )
+{
+    assert(hdu);
+    assert(keywords);
+    assert(item);
+
+    bool status = true;                 // Status of writing headers, to be returned
+    if (item->type == PS_DATA_LIST) {
+        psList *values = item->data.V;  // List of outputs
+        psList *keys = psStringSplit(keywords, " ,;", true); // List of keywords
+        if (keys->n != values->n && values->n != 0) {
+            psError(PS_ERR_UNKNOWN, true, "Number of keywords (%ld) does not match number of "
+                    "values (%ld).\n", keys->n, values->n);
+            psFree(keys);
+            return false;
+        }
+        psListIterator *keysIter = psListIteratorAlloc(keys, PS_LIST_HEAD, false); // Iterator for keywords
+        psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false); // Iterator for values
+        psString key = NULL;            // Keyword from iteration
+        while ((key = psListGetAndIncrement(keysIter))) {
+            psMetadataItem *value = psListGetAndIncrement(valuesIter); // Value from iteration; may be NULL
+            status &= writeSingleHeader(hdu, key, value);
+        }
+        psFree(keysIter);
+        psFree(valuesIter);
+        psFree(keys);
+    } else {
+        status = writeSingleHeader(hdu, keywords, item);
+    }
+    return status;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool p_pmConceptsWriteToCells(const psMetadata *specs, const pmCell *cell, const psMetadata *concepts)
+{
+    PS_ASSERT_PTR_NON_NULL(specs, false);
+    PS_ASSERT_PTR_NON_NULL(concepts, false);
+    if (!cell) {
+        return false;
+    }
+    if (!cell->config) {
+        return false;
+    }
+
+    pmHDU *hdu = pmHDUGetLowest(NULL, NULL, cell); // The HDU at the lowest level
+    if (!hdu) {
+        return false;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *specItem = NULL;    // Item from the specs metadata
+    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+        pmConceptSpec *spec = specItem->data.V; // The specification
+        psString name = specItem->name; // The concept name
+        psMetadataItem *cameraItem = psMetadataLookup(cell->config, name); // The concept from the camera,
+        // or NULL
+        if (cameraItem) {
+            // Grab the concept
+            psMetadataItem *conceptItem = psMetadataLookup(concepts, name); // The concept
+
+            psString nameSource = NULL; // String with the concept name and ".SOURCE" added
+            psStringAppend(&nameSource, "%s.SOURCE", name);
+            bool mdok = true;       // Status of MD lookup
+            psString source = psMetadataLookupStr(&mdok, cell->config, nameSource); // The source
+            if (mdok && strlen(source) > 0) {
+                psTrace("psModules.concepts", 8, "%s is %s\n", nameSource, source);
+                if (strcasecmp(source, "HEADER") == 0) {
+                    if (cameraItem->type != PS_DATA_STRING) {
+                        psWarning("Concept %s is specified by header, but is not "
+                                 "of type STR --- ignored.\n", conceptItem->name);
+                        continue;
+                    }
+
+                    // Formatted version
+                    psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_HEADER,
+                                                              cameraFormat, NULL, NULL, cell);
+                    if (!formatted) {
+                        continue;
+                    }
+
+                    psTrace("psModules.concepts", 8, "Writing %s to header %s\n", name, cameraItem->data.str);
+                    writeHeader(hdu, cameraItem->data.V, formatted);
+                    psFree(formatted);
+                } else if (strcasecmp(source, "VALUE") == 0) {
+                    // Formatted version
+                    psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_CELLS,
+                                                              cameraFormat, NULL, NULL, cell);
+                    if (!formatted) {
+                        continue;
+                    }
+
+                    psTrace("psModules.concepts", 8, "Checking %s against camera format.\n", name);
+                    if (! compareConcepts(formatted, cameraItem)) {
+                        psWarning("Concept %s is specified by value in the camera "
+                                 "format, but the values don't match.\n", name);
+                    }
+                    psFree(formatted);
+                } else {
+                    psWarning("Concept source %s isn't HEADER or VALUE --- can't "
+                             "write\n", nameSource);
+                }
+            } else {
+                // Assume it's specified by value
+                psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_CELLS,
+                                                          cameraFormat, NULL, NULL, cell);
+                if (!formatted) {
+                    continue;
+                }
+
+                if (! compareConcepts(formatted, cameraItem)) {
+                    psWarning("Concept %s is specified by value in the camera "
+                             "format, but the values don't match.\n", name);
+                }
+                psFree(formatted);
+            }
+            psFree(nameSource);
+        }
+
+    }
+    psFree(specsIter);
+    return true;
+}
+
+bool p_pmConceptsWriteToDefaults(const psMetadata *specs, const pmFPA *fpa, const pmChip *chip,
+                                 const pmCell *cell, psMetadata *concepts)
+{
+    PS_ASSERT_PTR_NON_NULL(specs, false);
+    PS_ASSERT_PTR_NON_NULL(concepts, false);
+
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+    if (!hdu) {
+        return false;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *defaults = psMetadataLookupMetadata(&mdok, cameraFormat, "DEFAULTS"); // The DEFAULTS spec
+    if (!mdok || !defaults) {
+        return false;
+    }
+
+    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *specItem = NULL;    // Item from the specs metadata
+    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+        pmConceptSpec *spec = specItem->data.V; // The specification
+        psString name = specItem->name; // The concept name
+        psMetadataItem *defaultItem = psMetadataLookup(defaults, name); // The item from the DEFAULTS
+        if (defaultItem) {
+            psMetadataItem *conceptItem = NULL; // The item from the concepts
+            if (defaultItem->type == PS_DATA_METADATA) {
+                // It's a menu --- need to look up the .DEPEND
+                psString dependName = NULL; // The concept name with ".DEPEND" on the end
+                psStringAppend(&dependName, "%s.DEPEND", defaultItem->name);
+                psString dependKey = psMetadataLookupStr(&mdok, defaults, dependName); // The keyword
+                if (!mdok || !dependKey || strlen(dependKey) == 0) {
+                    psWarning("Can't find %s in the DEFAULTS for %s --- ignored.\n",
+                             dependName, name);
+                    psFree(dependName);
+                    continue;
+                }
+                psString dependValue = psMetadataLookupStr(&mdok, concepts, dependKey); // The value
+                if (!mdok || !dependValue || strlen(dependValue) == 0) {
+                    psWarning("Concept %s specified by %s isn't of type STR -- "
+                             "ignored.\n", dependKey, dependName);
+                    psFree(dependName);
+                    continue;
+                }
+                // Get the actual item of interest, and correct the name to match the concept name
+                defaultItem = psMetadataLookup(defaultItem->data.md, dependValue);
+                if (!defaultItem) {
+                    psWarning("Concept dependency name %s is not found "
+                             "in concept table for %s -- ignored.\n", dependValue, dependName);
+                    psFree(dependName);
+                    continue;
+                }
+                defaultItem = psMetadataItemCopy(defaultItem);
+                psFree(dependName);
+                psFree(defaultItem->name);
+                defaultItem->name = psStringCopy(name);
+            } else {
+                psMemIncrRefCounter(defaultItem);
+            }
+            conceptItem = psMetadataLookup(concepts, name); // The item from the concepts
+            psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_DEFAULTS,
+                                                      cameraFormat, fpa, chip, cell);
+            if (!formatted) {
+                continue;
+            }
+            if (! compareConcepts(formatted, defaultItem)) {
+                psWarning("Concept %s is specified by the DEFAULTS in the camera "
+                         "format, but the values don't match.\n", name);
+            }
+            psFree(defaultItem);
+            psFree(formatted);
+        }
+    }
+    psFree(specsIter);
+    return true;
+}
+
+
+bool p_pmConceptsWriteToHeader(const psMetadata *specs, const pmFPA *fpa, const pmChip *chip,
+                               const pmCell *cell, const psMetadata *concepts)
+{
+    PS_ASSERT_PTR_NON_NULL(specs, false);
+    PS_ASSERT_PTR_NON_NULL(concepts, false);
+
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+    if (!hdu) {
+        return false;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *translation = psMetadataLookupMetadata(&mdok, cameraFormat, "TRANSLATION"); // The TRANSLATION spec
+    if (!mdok || !translation) {
+        return false;
+    }
+
+    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *specItem = NULL;    // Item from the specs metadata
+    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+        pmConceptSpec *spec = specItem->data.V; // The specification
+        psString name = specItem->name; // The concept name
+        psMetadataItem *headerItem = psMetadataLookup(translation, name); // The item from the TRANSLATION
+        if (headerItem) {
+            if (headerItem->type != PS_DATA_STRING) {
+                psWarning("TRANSLATION keyword for concept %s isn't of type STR ---"
+                         " ignored.", name);
+                continue;
+            }
+            psTrace("psModules.concepts", 3, "Writing %s to header %s\n", name, headerItem->data.str);
+            psMetadataItem *conceptItem = psMetadataLookup(concepts, name); // The item from the concepts
+            psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_HEADER,
+                                                      cameraFormat, fpa, chip, cell);
+            if (!formatted) {
+                continue;
+            }
+            writeHeader(hdu, headerItem->data.V, formatted);
+            psFree(formatted);
+        }
+    }
+    psFree(specsIter);
+    return true;
+}
+
+// XXX Warning: This code has not been tested at all
+bool p_pmConceptsWriteToDatabase(const psMetadata *specs, const pmFPA *fpa, const pmChip *chip,
+                                 const pmCell *cell, psDB *db, const psMetadata *concepts)
+{
+    PS_ASSERT_PTR_NON_NULL(specs, false);
+    PS_ASSERT_PTR_NON_NULL(concepts, false);
+
+    if (!db) {
+        return false;
+    }
+
+    #ifndef HAVE_PSDB
+    return false;
+    #else
+
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+    if (!hdu) {
+        return false;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *database = psMetadataLookupMetadata(&mdok, cameraFormat, "DATABASE"); // The DATABASE spec
+    if (!mdok || !database) {
+        return false;
+    }
+    psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *specItem = NULL;    // Item from the specs metadata
+    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+        pmConceptSpec *spec = specItem->data.V; // The specification
+        psString name = specItem->name; // The concept name
+
+        psMetadataItem *dbItem = psMetadataLookup(database, name); // The item from the DATABASE
+        if (dbItem) {
+            if (dbItem->type != PS_DATA_METADATA) {
+                psWarning("DATABASE keyword for concept %s isn't of type METADATA "
+                         "--- ignored.\n", name);
+                continue;
+            }
+
+            psMetadataItem *conceptItem = psMetadataLookup(concepts, name); // The item from the concepts
+            psMetadataItem *formatted = conceptFormat(spec, conceptItem, PM_CONCEPT_SOURCE_DATABASE,
+                                                      cameraFormat, fpa, chip, cell);
+            if (!formatted) {
+                continue;
+            }
+
+            psMetadata *dbLookup = dbItem->data.V; // How to look up the value of interest
+            // Name of the table
+            const char *tableName = psMetadataLookupStr(&mdok, dbLookup, "TABLE");
+            // Name of "where" columns
+            const char *givenCols = psMetadataLookupStr(&mdok, dbLookup, "GIVENDBCOL");
+            // Values for "where" columns
+            const char *givenPS = psMetadataLookupStr(&mdok, dbLookup, "GIVENPS");
+
+            // Now, need to get the "given"s
+            if (strlen(givenCols) || strlen(givenPS)) {
+                psList *cols = psStringSplit(givenCols, ",;", true); // List of column names
+                psList *values = psStringSplit(givenPS, ",;", true); // List of value names for the columns
+                psMetadata *selection = psMetadataAlloc(); // The stuff to select in the DB
+                if (cols->n != values->n) {
+                    psLogMsg(__func__, PS_LOG_WARN,
+                             "The GIVENDBCOL and GIVENPS entries for %s do not have "
+                             "the same number of entries --- ignored.\n", name);
+                } else {
+                    // Iterators for the lists
+                    psListIterator *colsIter = psListIteratorAlloc(cols, PS_LIST_HEAD, false);
+                    psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false);
+                    char *column = NULL;    // Name of the column
+                    while ((column = psListGetAndIncrement(colsIter))) {
+                        char *dependName = psListGetAndIncrement(valuesIter); // Name for the value
+                        if (!strlen(column) || !strlen(name)) {
+                            psWarning("One of the columns or value names for %s is "
+                                     " empty --- ignored.\n", name);
+                        } else {
+                            // Search for the value name
+                            psMetadataItem *item = NULL; // The value
+                            if (!item && cell) {
+                                item = psMetadataLookup(cell->concepts, dependName);
+                            }
+                            if (!item && chip) {
+                                item = psMetadataLookup(chip->concepts, dependName);
+                            }
+                            if (!item && fpa) {
+                                item = psMetadataLookup(fpa->concepts, dependName);
+                            }
+                            if (! item) {
+                                psLogMsg(__func__, PS_LOG_ERROR,
+                                         "Unable to find the value name %s for DB "
+                                         " lookup on %s --- ignored.\n", dependName, name);
+                            } else {
+                                // We need to create a new psMetadataItem.  I don't think we can't simply
+                                // hack the existing one, since that could conceivably cause memory leaks
+                                psMetadataAddItem(selection, formatted, PS_LIST_TAIL, PS_META_REPLACE);
+                                psFree(formatted);
+                            }
+                        }
+                        psFree(dependName);
+                        psFree(column);
+                    } // Iterating through the columns
+                    psFree(colsIter);
+                    psFree(valuesIter);
+
+                    // Check first to make sure we're only going to touch one row
+                    psArray *dbResult = psDBSelectRows(db, tableName, selection, 2); // Lookup result
+                    // Note that we use limit=2 in order to test if there are multiple rows returned
+                    if (! dbResult || dbResult->n == 0) {
+                        psWarning("Unable to find any rows in DB for %s --- "
+                                 "ignored\n", name);
+                        return false;
+                    } else {
+                        if (dbResult->n > 1) {
+                            psWarning("Multiple rows returned in DB lookup for %s "
+                                     "--- ignored.\n", name);
+                        }
+                        // Update the DB
+                        psMetadata *update = psMetadataAlloc();
+                        psMetadataAddItem(update, conceptItem, PS_LIST_HEAD, 0);
+                        psDBUpdateRows(db, tableName, selection, update);
+                        psFree(update);
+                        return true;
+                    }
+                }
+                psFree(cols);
+                psFree(values);
+            } // Doing the "given"s.
+        }
+    }
+    psFree(specsIter);
+    return true;
+    #endif
+}
+
Index: /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsWrite.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsWrite.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/concepts/pmConceptsWrite.h	(revision 15358)
@@ -0,0 +1,61 @@
+/* @file  pmConceptsWrite.h
+ * @brief Writing concepts to a variety of sources.
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONCEPTS_WRITE_H
+#define PM_CONCEPTS_WRITE_H
+
+/// @addtogroup Concepts Data Abstraction Concepts
+/// @{
+
+/// "Write" concepts to (actually, check against) the camera format file's CELLS.
+///
+/// Examines the CELLS metadata in the camera format file for the current type of cell, and checks that the
+/// concepts as defined there match the ones defined in the cell.  A warning is produced if the concepts do
+/// not match.
+bool p_pmConceptsWriteToCells(const psMetadata *specs, ///< The concept specifications
+                              const pmCell *cell, ///< The cell
+                              const psMetadata *concepts ///< The concepts
+                             );
+
+/// "Write" concepts to (actually, check against) the camera format file's DEFAULTS.
+///
+/// Examines the DEFAULTS metadata in the camera format file, and checks that the concepts as defined there
+/// match the ones defined in the cell.  A warning is produced if the concepts do not match.
+bool p_pmConceptsWriteToDefaults(const psMetadata *specs, ///< The concept specifications
+                                 const pmFPA *fpa, ///< The FPA
+                                 const pmChip *chip, ///< The chip
+                                 const pmCell *cell, ///< The cell
+                                 const psMetadata *concepts ///< The concepts
+                                );
+
+/// "Write" concepts to (actually, add to, pending a later write) the FITS header.
+///
+/// Examines the FITS header TRANSLATION metadata in the camera format file, and writes concepts to the
+/// appropriate FITS headers in the HDU, in preparation for a future write of the HDU.
+bool p_pmConceptsWriteToHeader(const psMetadata *specs, ///< The concept specifications
+                               const pmFPA *fpa, ///< The FPA
+                               const pmChip *chip, ///< The chip
+                               const pmCell *cell, ///< The cell
+                               const psMetadata *concepts ///< The concepts
+                              );
+
+/// Write concepts to the database.
+///
+/// Examines the DATABASE metadata in the camera format file, and writes concepts to the database.
+/// Warning: This function has not been tested; use at your own risk.
+bool p_pmConceptsWriteToDatabase(const psMetadata *specs, ///< The concept specifications
+                                 const pmFPA *fpa, ///< The FPA
+                                 const pmChip *chip, ///< The chip
+                                 const pmCell *cell, ///< The cell
+                                 psDB *db,///< The database handle
+                                 const psMetadata *concepts ///< The concepts
+                                );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/config/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/.cvsignore	(revision 15358)
@@ -0,0 +1,8 @@
+.deps
+.libs
+Makefile
+Makefile.in
+*.la
+*.lo
+pmErrorCodes.c
+pmErrorCodes.h
Index: /branches/eam_branch_20071023/psModules/src/config/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/Makefile.am	(revision 15358)
@@ -0,0 +1,33 @@
+noinst_LTLIBRARIES = libpsmodulesconfig.la
+
+libpsmodulesconfig_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS)
+libpsmodulesconfig_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulesconfig_la_SOURCES  = \
+    pmConfig.c \
+    pmConfigRecipes.c \
+    pmConfigCamera.c \
+    pmConfigCommand.c \
+    pmConfigMask.c \
+    pmVersion.c \
+    pmErrorCodes.c
+
+pkginclude_HEADERS = \
+    pmConfig.h \
+    pmConfigRecipes.h \
+    pmConfigCamera.h \
+    pmConfigCommand.h \
+    pmConfigMask.h \
+    pmVersion.h \
+    pmErrorCodes.h
+
+# Error codes.
+BUILT_SOURCES = pmErrorCodes.h pmErrorCodes.c
+CLEANFILES = *~ pmErrorCodes.h pmErrorCodes.c
+
+pmErrorCodes.h : pmErrorCodes.dat pmErrorCodes.h.in
+	$(ERRORCODES) --data=pmErrorCodes.dat --outdir=. pmErrorCodes.h
+
+pmErrorCodes.c : pmErrorCodes.dat pmErrorCodes.c.in pmErrorCodes.h
+	$(ERRORCODES) --data=pmErrorCodes.dat --outdir=. pmErrorCodes.c
+
+EXTRA_DIST = pmErrorCodes.h.in pmErrorCodes.dat pmErrorCodes.c.in
Index: /branches/eam_branch_20071023/psModules/src/config/pmConfig.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmConfig.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmConfig.c	(revision 15358)
@@ -0,0 +1,1447 @@
+/** @file  pmConfig.h
+ *
+ *  @author PAP (IfA)
+ *  @author EAM (IfA)
+ *
+ *  Copyright 2007 Institute for Astronomy, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>            /* for strn?casecmp */
+#include <unistd.h>
+#include <libgen.h>
+#include <assert.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <glob.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmErrorCodes.h"
+#include "pmFPALevel.h"
+#include "pmConfigRecipes.h"
+#include "pmConfigCamera.h"
+
+#ifdef HAVE_NEBCLIENT
+#include <nebclient.h>
+#endif // ifdef HAVE_NEBCLIENT
+
+#define PS_SITE "PS_SITE"         // Name of the environment variable containing the site config file
+#define PS_DEFAULT_SITE ".ipprc"  // Default site config file
+
+static bool readCameraConfig = true;    // Read the camera config on startup (with pmConfigRead)?
+static psArray *configPath = NULL;      // Search path for configuration files
+
+bool pmConfigReadParamsSet(bool newReadCameraConfig)
+{
+    bool oldReadCameraConfig = readCameraConfig;
+    readCameraConfig = newReadCameraConfig;
+    return oldReadCameraConfig;
+}
+
+static void configFree(pmConfig *config)
+{
+    psFree(config->site);
+    psFree(config->files);
+    psFree(config->camera);
+    psFree(config->cameraName);
+    psFree(config->format);
+    psFree(config->formatName);
+    psFree(config->recipes);
+    psFree(config->recipeSymbols);
+    psFree(config->arguments);
+    psFree(config->database);
+}
+
+pmConfig *pmConfigAlloc()
+{
+    pmConfig *config = psAlloc(sizeof(pmConfig));
+    (void)psMemSetDeallocator(config, (psFreeFunc)configFree);
+
+    // Initialise
+    config->site = NULL;
+    config->camera = NULL;
+    config->cameraName = NULL;
+    config->format = NULL;
+    config->formatName = NULL;
+    config->recipes = psMetadataAlloc();
+    config->recipesRead = PM_RECIPE_SOURCE_NONE;
+    config->recipeSymbols = psMetadataAlloc();
+    config->arguments = psMetadataAlloc();
+    config->database = NULL;
+    config->defaultRecipe = NULL;
+
+    // the file structure is used to carry pmFPAfiles
+    config->files = psMetadataAlloc ();
+    return config;
+}
+
+// Resolve environment variables within a directory name; returns the resolved directory string.
+// The returned string is likely a new pointer; the old pointer should be freed by psStringSubstitute.
+static psString resolveEnvVar(psString dir // Directory to check for environment variables
+                             )
+{
+    char *envStart;                     // Start of any environment variable
+    while ((envStart = strchr(dir, '$'))) {
+        char *envName = envStart + 1;   // Start of the environment variable name
+        if (envName[0] == '\0') {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Path %s contains a bad environment variable.\n", dir);
+            return NULL;
+        }
+        if (envName[0] == '{') {
+            envName++;
+            if (envName[0] == '\0') {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                        "Path %s contains a bad environment variable.\n", dir);
+                return NULL;
+            }
+        }
+        char *envStop = strpbrk(envStart, "}/"); // End of the environment variable
+        ssize_t nameLength = envStop ? envStop - envName : strlen(envName); // Length of the name
+        psString name = psStringNCopy(envName, nameLength); // The environment variable name
+        char *value = getenv(name);     // Value of the environment variable
+        psFree(name);
+        psString valueSlash = NULL;    // Value with appended slash
+        psStringAppend(&valueSlash, "%s/", value);
+
+        ssize_t envvarLength = envStop ? envStop - envStart : strlen(envStart); // Length, w/o '}'
+        psString envvar = psStringNCopy(envStart, envvarLength + 1);  // Environment variable, with $, {, }
+
+        psTrace("psModules.config", 7, "Replacing %s with %s in directory %s\n", envvar, valueSlash, dir);
+        psStringSubstitute(&dir, valueSlash, envvar);
+        psFree(envvar);
+        psFree(valueSlash);
+    }
+
+    return dir;
+}
+
+void pmConfigSet(const char *path)
+{
+    PS_ASSERT_STRING_NON_EMPTY(path,);
+
+    pmConfigDone();
+
+    psList *list = psStringSplit(path, ":", false);
+    configPath = psListToArray(list);
+    // Resolve environment variables
+    for (long i = 0; i < configPath->n; i++) {
+        configPath->data[i] = resolveEnvVar(configPath->data[i]);
+        psTrace("psModules.config", 4, "Path %ld: %s\n", i, (char*)configPath->data[i]);
+    }
+    psFree(list);
+}
+
+void pmConfigDone(void)
+{
+    psFree(configPath);
+    configPath = NULL;
+}
+
+bool pmConfigFileRead(psMetadata **config, const char *name, const char *description)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+    PS_ASSERT_STRING_NON_EMPTY(description, false);
+
+    char *realName = NULL;
+    unsigned int numBadLines = 0;
+    struct stat filestat;
+
+    psTrace("psModules.config", 3, "Loading %s configuration from file %s\n",
+            description, name);
+
+    uid_t uid = getuid();
+    gid_t gid = getgid();
+
+    // we try: name, path[0]/name, path[1]/name, ...
+    // find the first existing entry in the path (starting with the bare name)
+    realName = psStringCopy (name);
+    psTrace ("psModules.config", 8, "trying %s\n", realName);
+
+    int status = stat (realName, &filestat);
+    if (status == 0) {
+        if ((uid == filestat.st_uid) && (filestat.st_mode & S_IRUSR)) {
+            goto found;
+        }
+        if ((gid == filestat.st_gid) && (filestat.st_mode & S_IRGRP)) {
+            goto found;
+        }
+        if (filestat.st_mode & S_IROTH) {
+            goto found;
+        }
+    }
+    psFree (realName);
+
+    if (configPath == NULL) {
+        psError(PS_ERR_IO, true, "Cannot find %s configuration file (%s) in path\n", description, name);
+        return false;
+    }
+
+    for (int i = 0; i < configPath->n; i++) {
+        realName = psStringCopy (configPath->data[i]);
+        psStringAppend (&realName, "/%s", name);
+        psTrace ("psModules.config", 8, "trying %s\n", realName);
+
+        status = stat (realName, &filestat);
+        if (status == 0) {
+            if ((uid == filestat.st_uid) && (filestat.st_mode & S_IRUSR)) {
+                goto found;
+            }
+            if ((gid == filestat.st_gid) && (filestat.st_mode & S_IRGRP)) {
+                goto found;
+            }
+            if (filestat.st_mode & S_IROTH) {
+                goto found;
+            }
+        }
+        psFree (realName);
+    }
+
+    psError(PS_ERR_IO, true, "Cannot find %s configuration file %s in path\n", description, name);
+    return false;
+
+found:
+    *config = psMetadataConfigRead(NULL, &numBadLines, realName, true);
+    if (numBadLines > 0) {
+        psError(PS_ERR_IO, false, "%d bad lines in %s configuration file (%s)",
+                numBadLines, description, realName);
+        psFree (realName);
+
+        return false;
+    }
+    if (!*config) {
+        psError(PS_ERR_IO, true, "Unable to read %s configuration from %s",
+                description, realName);
+        psFree (realName);
+        return false;
+    }
+
+    psFree (realName);
+    return true;
+}
+
+// Read a file into a metadataItem, if required
+static bool metadataItemReadFile(psMetadataItem *item, // Item into which to read file
+                                 const char *description // Description, for error messages
+    )
+{
+    assert(item);
+    assert(description);
+
+    if (item->type == PS_DATA_METADATA) {
+        return true;                    // We've already read it
+    }
+    if (item->type != PS_DATA_STRING) {
+        psTrace("config", 2, "Element %s in %s metadata is not of type STR.\n",
+                item->name, description);
+        return false;
+    }
+
+    psTrace("config", 2, "Reading %s %s: %s\n", description, item->name, item->data.str);
+    psMetadata *new = NULL;         // New metadata
+    if (!pmConfigFileRead(&new, item->data.str, item->name)) {
+        psError(PM_ERR_CONFIG, false, "Trouble reading reading %s %s.\n",
+                description, item->name);
+        psFree(new);
+        return false;
+    }
+
+    // Muck around under the hood to replace the filename with the metadata; don't try this at home, kids
+    item->type = PS_DATA_METADATA;
+    psFree(item->data.str);
+    item->data.md = new;
+
+    return true;
+}
+
+// Read metadata config files in a metadata
+// The metadata contains file names, which will be replaced with the metadata that are in the files.
+static bool metadataReadFiles(psMetadata *source, // Source metadata
+                              const char *description // Description, for error messages
+                             )
+{
+    assert(source);
+    psMetadataIterator *iter = psMetadataIteratorAlloc(source, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        if (!metadataItemReadFile(item, description)) {
+            psError(PM_ERR_CONFIG, false, "Unable to read %s %s.", description, item->name);
+            psFree(iter);
+            return false;
+        }
+    }
+    psFree(iter);
+
+    return true;
+}
+
+// Read the formats for a camera
+static bool cameraReadFormats(psMetadata *camera, // Camera for which to read the formats
+                              const char *name // Name of the camera, for error messages
+                             )
+{
+    assert(camera);
+    assert(name);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *formats = psMetadataLookupMetadata(&mdok, camera, "FORMATS"); // Formats
+    if (!mdok || !formats) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find FORMATS in camera configuration %s.\n", name);
+        return false;
+    }
+    if (!metadataReadFiles(formats, "camera format")) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read formats within camera configuration %s.\n", name);
+        return false;
+    }
+
+    return true;
+}
+
+// Read the calibrations for a camera
+static bool cameraReadCalibrations(psMetadata *camera, // Camera for which to read the formats
+                                   const char *cameraName // Name of the camera, for error messages
+    )
+{
+    assert(camera);
+    assert(cameraName);
+
+    psMetadataItem *darkNorm = psMetadataLookup(camera, "DARK.NORM"); // The dark normalisation calibration
+    if (darkNorm) {
+        if (darkNorm->type == PS_DATA_STRING) {
+            const char *darkNormName = darkNorm->data.str; // The file name
+            psTrace("config", 2, "Reading %s dark normalisation: %s\n", cameraName, darkNormName);
+            psMetadata *new = NULL;         // New metadata
+            if (!pmConfigFileRead(&new, darkNormName, "Dark normalisation")) {
+                psError(PM_ERR_CONFIG, false, "Trouble reading reading %s dark normalisation %s --- "
+                        "ignored.\n", cameraName, darkNormName);
+                psFree(new);
+                return false;
+            }
+
+            // Muck around under the hood to replace the filename with the metadata;
+            // don't try this at home, kids
+            darkNorm->type = PS_DATA_METADATA;
+            psFree(darkNorm->data.str);
+            darkNorm->data.md = new;
+        } else if (darkNorm->type != PS_DATA_METADATA) {
+            psWarning("DARK.NORM in camera %s is not of type STR or METADATA (%x)", cameraName, darkNorm->type);
+        }
+    } else {
+        // Add a dummy entry
+        psPolynomial1D *poly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 1); // Dummy polynomial
+        poly->coeff[0] = 0.0;
+        poly->coeff[1] = 1.0;
+        psMetadata *polyMD = psMetadataAlloc(); // Container for the polynomial
+        (void)psPolynomial1DtoMetadata(polyMD, poly, "_DEFAULT"); // Metadata to insert
+        psFree(poly);
+        psMetadataAddMetadata(camera, PS_LIST_TAIL, "DARK.NORM", 0, "Dark normalisation polynomial",
+                              polyMD);
+        psMetadataAddStr(camera, PS_LIST_TAIL, "DARK.NORM.KEY", 0, "Key for dark normalisation", "_DEFAULT");
+        psFree(polyMD);
+    }
+
+    return true;
+}
+
+pmConfig *pmConfigRead(int *argc, char **argv, const char *defaultRecipe)
+{
+    PS_ASSERT_PTR_NON_NULL(argc, NULL);
+    PS_ASSERT_INT_POSITIVE(*argc, NULL);
+    PS_ASSERT_PTR_NON_NULL(argv, NULL);
+
+    pmConfig *config = pmConfigAlloc(); // The configuration, containing site, camera and recipes
+    config->defaultRecipe = defaultRecipe;
+
+    //
+    // The following section of code attempts to determine which file is
+    // the configuration file.  At the end of this code block, the siteName
+    // variable will contain the name of the configuration file.
+    //
+    char *siteName = NULL;
+    //
+    // First, try command line
+    //
+    psS32 argNum = psArgumentGet(*argc, argv, "-site");
+    if (argNum != 0) {
+        //
+        // We remove the "-site" argument from argv.  Then
+        // we look for the next argument, which should be the filename, and
+        // remove it as well.
+        //
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum >= *argc) {
+            psLogMsg("psModules.config", PS_LOG_WARN,
+                     "-site command-line switch provided without the required filename --- ignored.\n");
+        } else {
+            siteName = psStringCopy(argv[argNum]);
+            psArgumentRemove(argNum, argc, argv);
+        }
+    }
+    //
+    // Next, try environment variable
+    //
+    if (!siteName) {
+        siteName = getenv(PS_SITE);
+        if (siteName) {
+            siteName = psStringCopy (siteName);
+        }
+    }
+
+    //
+    // Last chance is ~/.ipprc
+    //
+    if (!siteName) {
+        char *home = getenv("HOME");
+        siteName = psStringCopy(home);
+        psStringAppend(&siteName, "/%s", PS_DEFAULT_SITE);
+    }
+
+
+    // We have the configuration filename; now we read and parse the config
+    // file and store in psMetadata struct site.
+    //
+
+    if (!pmConfigFileRead(&config->site, siteName, "site")) {
+        psFree(config);
+        return NULL;
+    }
+
+    // Set options based on the site configuration.
+    {
+        bool mdok = true;   // Status of MD lookup result
+
+        // Initialise the psLib time handling
+        psString timeName = psMetadataLookupStr(&mdok, config->site, "TIME");
+        if (mdok && timeName)
+        {
+            psTrace("psModules.config", 7, "Initialising psTime with file %s\n", timeName);
+            psTimeInit(timeName);
+        }
+
+
+        // Set logging level
+        int logLevel = psMetadataLookupS32(&mdok, config->site, "LOGLEVEL");
+        if (mdok && logLevel >= 0)
+        {
+            psTrace("psModules.config", 7, "Setting log level to %d\n", logLevel);
+            psLogSetLevel(logLevel);
+        }
+
+
+        // Set logging format
+        psString logFormat = psMetadataLookupStr(&mdok, config->site, "LOGFORMAT");
+        if (mdok && logFormat)
+        {
+            psTrace("psModules.config", 7, "Setting log format to %s\n", logFormat);
+            psLogSetFormat(logFormat);
+        }
+
+        argNum = psArgumentGet(*argc, argv, "-log");
+        if (argNum > 0)
+        {
+            psArgumentRemove(argNum, argc, argv);
+            if (argNum >= *argc) {
+                psLogMsg("psModules.config", PS_LOG_WARN, "-log command-line switch provided without the "
+                         "required log destination --- ignored.\n");
+            } else {
+                if (!psLogSetDestination(psMessageDestination(argv[argNum]))) {
+                    psLogMsg("psModules.config", PS_LOG_WARN, "Unable to set log destination to %s\n",
+                             argv[argNum]);
+                }
+                psArgumentRemove(argNum, argc, argv);
+            }
+        } else
+        {
+            // Set logging destination
+            psString logDest = psMetadataLookupStr(&mdok, config->site, "LOGDEST");
+            if (mdok && logDest && strlen(logDest) > 0) {
+                // XXX: Only stdout and stderr are provided for now; this section should be
+                // expanded in the future to do files, and perhaps even sockets.
+                if (!psLogSetDestination(psMessageDestination(logDest))) {
+                    psLogMsg("psModules.config", PS_LOG_WARN, "Unable to set log destination to %s\n",
+                             argv[argNum]);
+                }
+            }
+        }
+
+        // Set trace levels
+        psMetadata *trace = psMetadataLookupMetadata(&mdok, config->site, "TRACE");
+        if (mdok && trace)
+        {
+            psMetadataIterator *traceIter = psMetadataIteratorAlloc(trace, PS_LIST_HEAD, NULL); // Iterator
+            psMetadataItem *traceItem = NULL; // Item from MD iteration
+            while ((traceItem = psMetadataGetAndIncrement(traceIter))) {
+                if (traceItem->type != PS_DATA_S32) {
+                    psLogMsg("psModules.config", PS_LOG_WARN,
+                             "The level for trace component %s is not of type S32 (%x)\n",
+                             traceItem->name, traceItem->type);
+                    continue;
+                }
+                psTrace("psModules.config", 7, "Setting trace level for %s to %d\n", traceItem->name, traceItem->data.S32);
+                (void)psTraceSetLevel(traceItem->name, traceItem->data.S32);
+            }
+            psFree(traceIter);
+        }
+
+        // Set trace formats
+        psString traceFormat = psMetadataLookupStr(&mdok, config->site, "TRACEFORMAT");
+        if (mdok && traceFormat)
+        {
+            psTrace("psModules.config", 7, "Setting trace format to %s\n", traceFormat);
+            (void)psTraceSetFormat(traceFormat);
+        }
+
+        // Set trace destinations
+        #ifndef PS_NO_TRACE
+        psString traceDest = psMetadataLookupStr(&mdok, config->site, "TRACEDEST");
+        if (mdok && traceDest && strlen(traceDest) > 0)
+        {
+            psTrace("psModules.config", 7, "Setting trace destination to %s\n", traceDest);
+            // XXX: Only stdout and stderr are provided for now; this section should be
+            // expanded in the future to do files, and perhaps even sockets.
+            if (!psTraceSetDestination(psMessageDestination(traceDest))) {
+                psLogMsg("psModules.config", PS_LOG_WARN, "Unable to set trace destination to %s\n", traceDest);
+
+            }
+        }
+        #endif
+
+        // Allow command line options to override defaults for logging.
+        // XXX: Is it appropriate to use the ArgVerbosity function for this?
+        //   A: it removes the options from the command line.
+        //   B: will the pmConfigRead function always be called on initialization.
+        //
+        psArgumentVerbosity(argc, argv);
+        // XXX: substitute the string for the default log level for "2".
+    }
+
+    // define the config-file search path (configPath).  Ensure that
+    // it contains the directory where we found the config file in
+    // the first place
+    if (configPath) {
+        pmConfigDone();
+    }
+
+    psString siteNameDir = psStringCopy(dirname(siteName));
+    psFree(siteName);
+
+    psString path = psMetadataLookupStr(NULL, config->site, "PATH");
+    if (path) {
+        psStringAppend(&path, ":%s", siteNameDir);
+    } else {
+        path = psMemIncrRefCounter(siteNameDir);
+    }
+    pmConfigSet(path);
+    psFree(siteNameDir);
+
+    // Next, we do a similar thing for the camera configuration file.  The
+    // file is read and parsed into psMetadata struct "camera".
+    //
+    argNum = psArgumentGet(*argc, argv, "-camera");
+    if (argNum > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum >= *argc) {
+            psWarning("-camera command-line switch provided without the required camera or filename --- "
+                      "ignored.\n");
+        } else {
+            bool mdok = true;           // Status of MD lookup
+            char *cameraName = argv[argNum]; // symbolic name of the camera
+
+            // look for the CAMERAS list in config->site
+            psMetadata *cameras = psMetadataLookupMetadata(&mdok, config->site, "CAMERAS");
+            if (!cameras) {
+                psError(PS_ERR_IO, false, "Unable to find CAMERAS in site configuration.\n");
+                psFree(config);
+                return NULL;
+            }
+
+            // look for the symbolic camera name in the CAMERAS metadata
+            char *cameraFile = psMetadataLookupStr(&mdok, cameras, cameraName); // The filename
+            if (!cameraFile) {
+                psError(PS_ERR_IO, false, "%s is not listed in the site CAMERAS list\n", cameraName);
+                psFree(config);
+                return NULL;
+            }
+
+            // load this camera's configuration informatoin
+            if (!pmConfigFileRead(&config->camera, cameraFile, "camera")) {
+                psError(PM_ERR_CONFIG, false, "Problem reading %s", cameraName);
+                psFree(config);
+                return NULL;
+            }
+            // save the name for future uses
+            config->cameraName = psStringCopy (cameraName);
+
+            psArgumentRemove(argNum, argc, argv);
+
+            // Read in the formats
+            if (!cameraReadFormats(config->camera, cameraFile)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to read formats within camera configuration %s.\n",
+                        cameraFile);
+                psFree(config);
+                return NULL;
+            }
+
+            // Read in any camera-specific calibrations
+            if (!cameraReadCalibrations(config->camera, cameraName)) {
+                psError(PS_ERR_UNKNOWN, false,
+                        "Unable to read calibrations within camera configuration %s.\n",
+                        cameraName);
+                psFree(config);
+                return NULL;
+            }
+
+            psMetadataAddMetadata(cameras, PS_LIST_HEAD, cameraName, PS_META_REPLACE,
+                                  "Camera specified on command line", config->camera);
+
+            if (!pmConfigCameraSkycellVersion(config->site, cameraName)) {
+                psError(PS_ERR_UNKNOWN, false,
+                        "Unable to generate skycell versions of specified camera %s.\n",
+                        cameraName);
+                psFree(config);
+                return NULL;
+            }
+
+            if (!pmConfigCameraMosaickedVersions(config->site, cameraName)) {
+                psError(PS_ERR_UNKNOWN, false,
+                        "Unable to generate mosaicked versions of specified camera %s.\n",
+                        cameraName);
+                psFree(config);
+                return NULL;
+            }
+        }
+    }
+
+    // Read the camera configurations, if not already defined, and not turned off
+    if (!config->camera && readCameraConfig) {
+        bool mdok;                      // Status of MD lookup
+        psMetadata *cameras = psMetadataLookupMetadata(&mdok, config->site, "CAMERAS"); // List of cameras
+        if (!mdok || !cameras) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the site configuration.\n");
+            return false;
+        }
+
+        if (!metadataReadFiles(cameras, "camera configuration")) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to read cameras within site configuration.\n");
+            psFree(config);
+            return NULL;
+        }
+
+        // Now fill in the formats and calibrations
+        psMetadataIterator *iter = psMetadataIteratorAlloc(cameras, PS_LIST_HEAD, NULL); // Iterator
+        psMetadataItem *item;           // Item from iteration
+        while ((item = psMetadataGetAndIncrement(iter))) {
+            assert(item->type == PS_DATA_METADATA);
+            if (!cameraReadFormats(item->data.md, item->name)) {
+                psWarning("Unable to read formats for camera %s: removed.\n", item->name);
+                psMetadataRemoveKey(cameras, item->name);
+            }
+            if (!cameraReadCalibrations(item->data.md, item->name)) {
+                psWarning("Unable to read calibrations for camera %s: removed.\n", item->name);
+                psMetadataRemoveKey(cameras, item->name);
+            }
+        }
+        psFree(iter);
+
+        if (!pmConfigCameraSkycellVersionsAll(config->site)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to generate skycell versions of cameras.\n");
+            psFree(config);
+            return NULL;
+        }
+        if (!pmConfigCameraMosaickedVersionsAll(config->site)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to generate mosaicked versions of cameras.\n");
+            psFree(config);
+            return NULL;
+        }
+    }
+
+    // Load the recipes from the camera file, if appropriate
+    if(!pmConfigReadRecipes(config, PM_RECIPE_SOURCE_SITE | PM_RECIPE_SOURCE_CAMERA)) {
+        psError(PS_ERR_IO, false, "Failed to read recipes from camera file");
+        psFree(config);
+        return NULL;
+    }
+
+    // load command-line options of the form -recipe NAME RECIPE
+    pmConfigLoadRecipeArguments(argc, argv, config);
+
+    // read in command-line options to specific recipe values
+    pmConfigLoadRecipeOptions(argc, argv, config, "-D");
+    pmConfigLoadRecipeOptions(argc, argv, config, "-Di");
+    pmConfigLoadRecipeOptions(argc, argv, config, "-Df");
+    pmConfigLoadRecipeOptions(argc, argv, config, "-Db");
+
+    // Look for command-line options for files to replace
+    argNum = psArgumentGet(*argc, argv, "-F");
+    if (argNum > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum + 1 >= *argc) {
+            psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                    "Filerule switch (-F) provided without old and new filerule.");
+            psFree(config);
+            return NULL;
+        }
+
+        const char *old = argv[argNum]; // The old file, to be replaced
+        psArgumentRemove(argNum, argc, argv);
+        const char *new = argv[argNum]; // The new file, the replacement
+        psArgumentRemove(argNum, argc, argv);
+
+        psMetadata *cameras = psMetadataLookupMetadata(NULL, config->site, "CAMERAS"); // List of cameras
+        if (!cameras) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find CAMERAS in the site configuration.\n");
+            return false;
+        }
+
+        psMetadataIterator *camerasIter = psMetadataIteratorAlloc(cameras, PS_LIST_HEAD, NULL); // Iterator
+        psMetadataItem *cameraItem;     // Item from iteration
+        while ((cameraItem = psMetadataGetAndIncrement(camerasIter))) {
+            // Silently ignore problems --- they will be caught later, because if the user wants the nominated
+            // file and it's not available for that camera, then they will know.
+
+            if (cameraItem->type != PS_DATA_METADATA) {
+                psTrace("psModules.config", 2,
+                        "Entry %s in CAMERAS is not of type METADATA --- ignored.", cameraItem->name);
+                continue;
+            }
+            psMetadata *camera = cameraItem->data.md; // Camera configuration
+
+            psMetadata *newRule = pmConfigFileRule(config, camera, new); // The rule of interest
+            if (!newRule) {
+                psTrace("psModules.config", 2,
+                        "Unable to find filerule %s in camera %s --- ignored.", new, cameraItem->name);
+                continue;
+            }
+
+            // By calling pmConfigFileRule, we've assured that the FILERULES is now a metadata
+            psMetadata *filerules = psMetadataLookupMetadata(NULL, camera, "FILERULES"); // File rules
+            if (!filerules) {
+                psTrace("psModules.config", 2,
+                        "Can't find FILERULES of type METADATA in camera %s --- ignored.", cameraItem->name);
+                continue;
+            }
+
+            psMetadataAddMetadata(filerules, PS_LIST_TAIL, old, PS_META_REPLACE,
+                                  "Original replaced by -F option", newRule);
+        }
+        psFree(camerasIter);
+    }
+
+    // check for values that override DB* keywords
+    argNum = psArgumentGet(*argc, argv, "-dbserver");
+    if (argNum > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum >= *argc) {
+            psLogMsg("psModules.config", PS_LOG_WARN,
+                     "-dbserver command-line switch provided without the required server name --- ");
+        } else {
+            char *dbserver = argv[argNum]; // The camera configuration file to read
+            if (!psMetadataAddStr(config->site, PS_LIST_TAIL, "DBSERVER", PS_META_REPLACE, NULL, dbserver)) {
+                psLogMsg("psModules.config", PS_LOG_WARN,
+                         "failed to overwrite .ipprc DBSERVER value --- ");
+            }
+
+            psArgumentRemove(argNum, argc, argv);
+        }
+    }
+
+    argNum = psArgumentGet(*argc, argv, "-dbname");
+    if (argNum > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum >= *argc) {
+            psLogMsg("psModules.config", PS_LOG_WARN,
+                     "-dbname command-line switch provided without the required database name --- ");
+        } else {
+            char *dbname = argv[argNum]; // The camera configuration file to read
+            if (!psMetadataAddStr(config->site, PS_LIST_TAIL, "DBNAME", PS_META_REPLACE, NULL, dbname)) {
+                psLogMsg("psModules.config", PS_LOG_WARN,
+                         "failed to overwrite .ipprc DBNAME value --- ");
+            }
+
+            psArgumentRemove(argNum, argc, argv);
+        }
+    }
+
+    argNum = psArgumentGet(*argc, argv, "-dbuser");
+    if (argNum > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum >= *argc) {
+            psLogMsg("psModules.config", PS_LOG_WARN,
+                     "-dbuser command-line switch provided without the required database name --- ");
+        } else {
+            char *dbuser = argv[argNum]; // The camera configuration file to read
+            if (!psMetadataAddStr(config->site, PS_LIST_TAIL, "DBUSER", PS_META_REPLACE, NULL, dbuser)) {
+                psLogMsg("psModules.config", PS_LOG_WARN,
+                         "failed to overwrite .ipprc DBUSER value --- ");
+            }
+
+            psArgumentRemove(argNum, argc, argv);
+        }
+    }
+
+    argNum = psArgumentGet(*argc, argv, "-dbpassword");
+    if (argNum > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum >= *argc) {
+            psLogMsg("psModules.config", PS_LOG_WARN,
+                     "-dbpassword command-line switch provided without the required password --- ");
+        } else {
+            char *dbpassword = argv[argNum]; // The camera configuration file to read
+            if (!psMetadataAddStr(config->site, PS_LIST_TAIL, "DBPASSWORD", PS_META_REPLACE, NULL, dbpassword)) {
+                psLogMsg("psModules.config", PS_LOG_WARN,
+                         "failed to overwrite .ipprc DBPASSWORD value --- ");
+            }
+
+            psArgumentRemove(argNum, argc, argv);
+        }
+    }
+
+    argNum = psArgumentGet(*argc, argv, "-dbport");
+    if (argNum > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum >= *argc) {
+            psLogMsg("psModules.config", PS_LOG_WARN,
+                     "-dbpport command-line switch provided without the required port number --- ");
+        } else {
+            char *dbport = argv[argNum]; // The camera configuration file to read
+            if (!psMetadataAddS32(config->site, PS_LIST_TAIL, "DBPORT", PS_META_REPLACE, NULL, (psS32)atoi(dbport))) {
+                psLogMsg("psModules.config", PS_LOG_WARN,
+                         "failed to overwrite .ipprc DBPORT value --- ");
+            }
+
+            psArgumentRemove(argNum, argc, argv);
+        }
+    }
+
+
+    psErrorClear();   // we may have failed to find some items in the metadata
+
+    return config;
+}
+
+
+// does this header match the specified camera format?  answer is supplied to 'valid' the
+// return value defines the error condition. error only on config errors
+bool pmConfigValidateCameraFormat(bool *valid, const psMetadata *cameraFormat, const psMetadata *header)
+{
+    PS_ASSERT_PTR_NON_NULL(cameraFormat, false);
+    PS_ASSERT_PTR_NON_NULL(header, false);
+
+    // Read the rule for that camera format
+    bool mdStatus = true;
+
+    psMetadata *rule = psMetadataLookupMetadata(&mdStatus, cameraFormat, "RULE");
+    if (! mdStatus || ! rule) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read rule for camera.");
+        *valid = false;
+        return false;
+    }
+
+    // grab the metadata items in sequence by key so we get the MULTI entry
+    psList *keyList = psMetadataKeys (rule);
+    psArray *keys = psListToArray (keyList);
+    if (! keys) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read rule for camera.");
+        *valid = false;
+        return false;
+    }
+
+    *valid = true;
+    for (int i = 0; *valid && (i < keys->n); i++) {
+
+        // get the ruleItem for this key
+        psMetadataItem *ruleItem = psMetadataLookup(rule, keys->data[i]);
+
+        // Check for the existence of the rule in the header
+        psMetadataItem *headerItem = psMetadataLookup(header, ruleItem->name);
+        if (! headerItem) {
+            // rule item not found in header
+            psTrace("psModules.config", 5, "Can't find %s", ruleItem->name);
+            *valid = false;
+            continue;
+        }
+
+        // if the RULE type is a primitive type (int, float, etc) or string compare directly
+        if (PS_DATA_IS_PRIMITIVE (ruleItem->type) || (ruleItem->type == PS_DATA_STRING)) {
+            // Check to see if the rule works
+            if (!psMetadataItemCompare(headerItem, ruleItem)) {
+                psTrace("psModules.config", 5, "%s doesn't match.", ruleItem->name);
+                *valid = false;
+            }
+            continue;
+        }
+
+        // for MULTI, try each one & succeed if any match (valid = true is default state)
+        if (ruleItem->type == PS_DATA_METADATA_MULTI) {
+            bool found = false;
+            for (int j = 0; j < ruleItem->data.list->n; j++) {
+                psMetadataItem *entry = psListGet (ruleItem->data.list, j);
+                assert (entry);
+                if (psMetadataItemCompare(headerItem, entry)) {
+                    found = true;
+                    psTrace("psModules.config", 5, "%s in multi list matches.", ruleItem->name);
+                    break;
+                }
+            }
+            if (!found) {
+                *valid = false;
+                psTrace("psModules.config", 5, "%s doesn't match.", ruleItem->name);
+            }
+            continue;
+        }
+
+        psError(PS_ERR_UNKNOWN, false, "Invalid type for RULE %s.", ruleItem->name);
+        *valid = false;
+        psFree (keyList);
+        psFree (keys);
+        return false;
+    }
+
+    psFree (keyList);
+    psFree (keys);
+    return true;
+}
+
+// Given a camera and a header, see if any of the camera formats match the header
+static bool formatFromHeader(psMetadata **format, // Format to return
+                             psString *name, // Name to return
+                             psMetadata *camera, // Camera configuration
+                             const psMetadata *header, // FITS header
+                             const char *cameraName // Name of camera
+                            )
+{
+    assert(format);
+    assert(camera);
+    assert(header);
+    assert(cameraName);
+    assert(*cameraName);
+
+    bool result = false;                // Did we find the first match?
+
+    // Read the list of formats
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *formats = psMetadataLookupMetadata(&mdok, camera, "FORMATS"); // List of formats
+    if (!mdok || !formats) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find list of FORMATS in camera %s", cameraName);
+        return false;
+    }
+
+    if (!metadataReadFiles(formats, "camera format")) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to read cameras formats within camera configuration.\n");
+        return false;
+    }
+
+    // Iterate over the formats
+    psMetadataIterator *formatsIter = psMetadataIteratorAlloc(formats, PS_LIST_HEAD, NULL);
+    psMetadataItem *formatsItem = NULL; // Item from formats
+    while ((formatsItem = psMetadataGetAndIncrement(formatsIter))) {
+        assert(formatsItem->type == PS_DATA_METADATA); // Since we have just read it in or deleted it
+        psMetadata *testFormat = formatsItem->data.md; // Format to test against
+
+        psTrace("psModules.config", 5, "trying format %s", formatsItem->name);
+
+        bool valid = false;
+        if (!pmConfigValidateCameraFormat(&valid, testFormat, header)) {
+            psError (PS_ERR_UNKNOWN, false, "Error in config scripts for camera %s, format %s\n",
+                     cameraName, formatsItem->name);
+            return false;
+        }
+        if (valid) {
+            if (!*format) {
+                psLogMsg("psModules.config", PS_LOG_INFO, "Camera %s, format %s matches header.\n",
+                         cameraName, formatsItem->name);
+                *format = psMemIncrRefCounter(testFormat);
+                *name = psStringCopy(formatsItem->name);
+                result = true;
+            } else {
+                psLogMsg("psModules.config", PS_LOG_WARN,
+                         "Camera %s, format %s also matches header --- ignored.\n",
+                         cameraName, formatsItem->name);
+            }
+        }
+    }
+    psFree(formatsIter);
+    return result;
+}
+
+// we only need to read the recipe for the main camera images.  for additional images (eg, sky
+// cells) do not need to re-read the recipe files!
+psMetadata *pmConfigCameraFormatFromHeader(pmConfig *config, const psMetadata *header, bool readRecipes)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(header, NULL);
+
+    psMetadata *format = NULL;          // The winning format
+    psString name = NULL;               // Name of the winning format
+
+    // If we don't know what sort of camera we have, we try all that we know
+    if (! config->camera) {
+        bool mdok;                      // Metadata lookup status
+        psMetadata *cameras = psMetadataLookupMetadata(&mdok, config->site, "CAMERAS");
+        if (! mdok || !cameras) {
+            psError(PS_ERR_IO, true, "Unable to find CAMERAS in the configuration.");
+            return NULL;
+        }
+
+        if (!metadataReadFiles(cameras, "camera configuration")) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to read cameras within site configuration.\n");
+            return NULL;
+        }
+
+        // Iterate over the cameras
+        psMetadataIterator *camerasIter = psMetadataIteratorAlloc(cameras, PS_LIST_HEAD, NULL);
+        psMetadataItem *camerasItem = NULL; // Item from the metadata
+        while ((camerasItem = psMetadataGetAndIncrement(camerasIter))) {
+            // Open the camera information
+            psTrace("psModules.config", 3, "Inspecting camera %s (%s)\n", camerasItem->name,
+                    camerasItem->comment);
+            assert(camerasItem->type == PS_DATA_METADATA); // It should be because we've read it in or deleted
+            psMetadata *testCamera = camerasItem->data.md; // Camera to test against what we've got:
+            if (formatFromHeader(&format, &name, testCamera, header, camerasItem->name)) {
+                // Set the defaults, if they are not already set
+                if (!config->camera && !config->cameraName) {
+                    config->camera = psMemIncrRefCounter(testCamera);
+                    config->cameraName = psStringCopy(camerasItem->name);
+                }
+                if (!config->format && !config->formatName) {
+                    config->formatName = name;
+                    config->format = format;
+                } else {
+                    psFree(name);
+                    psFree(format);
+                }
+            } else {
+                psErr *error = psErrorLast();
+                if (error->code != PS_ERR_NONE) {
+                    psError(PS_ERR_IO, false, "Error reading camera config data for %s", camerasItem->name);
+                    return NULL;
+                }
+                psFree(error);
+            }
+        } // Done looking at all cameras
+        psFree(camerasIter);
+
+        if (! config->camera) {
+            psError(PS_ERR_IO, false, "Unable to find a camera that matches input FITS header!");
+            return NULL;
+        }
+
+        // Now we have the camera, we can read the recipes
+        if (readRecipes && !pmConfigReadRecipes(config, PM_RECIPE_SOURCE_CAMERA | PM_RECIPE_SOURCE_CL)) {
+            psError(PS_ERR_IO, false, "Error reading recipes from camera config for %s", config->cameraName);
+            return NULL;
+        }
+        return psMemIncrRefCounter(format);
+    }
+
+    // Otherwise, try the specific camera
+    if (!formatFromHeader(&format, &name, config->camera, header, config->cameraName)) {
+        psError(PS_ERR_IO, true, "Unable to find a format with the specified camera (%s) that matches the "
+                "given header.\n", config->cameraName);
+        return NULL;
+    }
+
+    if (!config->format && !config->formatName) {
+        config->formatName = name;
+        config->format = format;
+    } else {
+        psFree(format);
+        psFree(name);
+    }
+    return psMemIncrRefCounter(format);
+}
+
+// Return the requested camera configuration
+psMetadata *pmConfigCameraByName(pmConfig *config, const char *cameraName)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(cameraName, NULL);
+
+    psMetadata *cameras = psMetadataLookupMetadata(NULL, config->site, "CAMERAS");
+    if (!cameras) {
+        psError(PS_ERR_IO, true, "Unable to find CAMERAS in the configuration.");
+        return NULL;
+    }
+
+    char *cameraPath = psMetadataLookupStr(NULL, cameras, cameraName);
+    if (!cameraPath) {
+        psError(PS_ERR_IO, true, "Unable to find requested CAMERA in the configuration.");
+        return NULL;
+    }
+
+    psMetadata *camera = NULL; // Camera to test against what we've got:
+
+    if (!pmConfigFileRead(&camera, cameraPath, cameraName)) {
+        psLogMsg("psModules.config", PS_LOG_WARN, "Trouble reading reading camera configuration %s", cameraName);
+        psFree(camera);
+        return NULL;
+    }
+
+    return camera;
+}
+
+psDB *pmConfigDB(pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(config->site, NULL);
+
+    #ifndef HAVE_PSDB
+
+    psError(PS_ERR_UNKNOWN, false,
+            "Cannot configure database: psModules was compiled without database support.");
+    return NULL;
+
+    #else
+
+    psBool mdStatus01 = false;
+    psBool mdStatus02 = false;
+    psBool mdStatus03 = false;
+    psBool mdStatus04 = false;
+    psBool mdStatus05 = false;
+
+    // XXX leaky strings
+    psString dbServer = psMetadataLookupStr(&mdStatus01, config->site, "DBSERVER");
+    psString dbUsername = psMetadataLookupStr(&mdStatus02, config->site, "DBUSER");
+    psString dbPassword = psMetadataLookupStr(&mdStatus03, config->site, "DBPASSWORD");
+    psString dbName = psMetadataLookupStr(&mdStatus04, config->site, "DBNAME");
+    psS32 dbPort = psMetadataLookupS32(&mdStatus05, config->site, "DBPORT");
+    if (!mdStatus05) {
+        dbPort = 0;
+    }
+    if (!(mdStatus01 && mdStatus02 && mdStatus03 && mdStatus04)) {
+        psLogMsg("psModules.config", PS_LOG_WARN,
+                 "Could not determine database server, name, user, and password from site metadata.\n");
+        return NULL;
+    }
+
+    return psDBInit(dbServer, dbUsername, dbPassword, dbName, dbPort);
+    #endif
+}
+
+
+bool pmConfigConformHeader(psMetadata *header, const psMetadata *format)
+{
+    PS_ASSERT_PTR_NON_NULL(header, false);
+    PS_ASSERT_PTR_NON_NULL(format, false);
+
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *rules = psMetadataLookupMetadata(&mdok, format, "RULE"); // How to identify this format
+    if (!mdok || !rules) {
+        psError(PS_ERR_IO, true, "Unable to find RULE in camera format.\n");
+        return false;
+    }
+
+    psMetadataIterator *rulesIter = psMetadataIteratorAlloc(rules, PS_LIST_HEAD, NULL); // Iterator for rules
+    psMetadataItem *rulesItem = NULL;   // Item from iteration
+    while ((rulesItem = psMetadataGetAndIncrement(rulesIter))) {
+        // this will insert each of the MULTI entries, writing over the previous copies
+        if (PS_DATA_IS_PRIMITIVE (rulesItem->type) || (rulesItem->type == PS_DATA_STRING)) {
+            psMetadataItem *newItem = psMetadataItemCopy(rulesItem); // Copy of item
+            psMetadataAddItem(header, newItem, PS_LIST_TAIL, PS_META_REPLACE);
+            psFree(newItem);                // Drop reference
+            continue;
+        }
+
+        # if (0) // XXX not needed
+        // for MULTI entries, supply the first one
+        if (rulesItem->type == PS_DATA_METADATA_MULTI) {
+            psMetadataItem *entry = psListGet (rulesItem->data.list, PS_LIST_HEAD);
+            if (!entry) {
+                psError(PS_ERR_UNKNOWN, true, "No entries for MULTI RULE item.");
+                return false;
+            }
+            psMetadataItem *newItem = psMetadataItemCopy(entry); // Copy of item
+            psMetadataAddItem(header, newItem, PS_LIST_TAIL, PS_META_REPLACE);
+            psFree(newItem);                // Drop reference
+            continue;
+        }
+        # endif
+
+        psError(PS_ERR_UNKNOWN, false, "Invalid type for RULE %s.", rulesItem->name);
+        return false;
+    }
+    psFree(rulesIter);
+
+    return true;
+}
+
+psArray *pmConfigFileSets(int *argc, char **argv, const char *file, const char *list)
+{
+    PS_ASSERT_PTR_NON_NULL(argc, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(*argc, NULL);
+    PS_ASSERT_PTR_NON_NULL(argv, NULL);
+
+    int Narg;
+
+    // we load all input files onto a psArray, to be parsed later
+    psArray *input = psArrayAllocEmpty(16);
+
+    // load the list of filenames the supplied file
+    // maybe a comma-separated list of words
+    // each word may be a glob: "file*.fits"
+    if (file && strlen(file) > 0 && (Narg = psArgumentGet (*argc, argv, file))) {
+
+        // select the word after 'file' and split by comma
+        psArgumentRemove (Narg, argc, argv);
+        psArray *words = psStringSplitArray (argv[Narg], ",", true);
+        psArgumentRemove (Narg, argc, argv);
+
+        // parse the word as a glob
+        glob_t globList;
+        for (int i = 0; i < words->n; i++) {
+            globList.gl_offs = 0;
+            glob (words->data[i], 0, NULL, &globList);
+
+            // if the glob does not match, save the literal word:
+            // otherwise save all glob matches
+            if (globList.gl_pathc == 0) {
+                psArrayAdd (input, 16, words->data[i]);
+            } else {
+                for (int j = 0; j < globList.gl_pathc; j++) {
+                    char *filename = psStringCopy (globList.gl_pathv[j]);
+                    psArrayAdd (input, 16, filename);
+                    psFree (filename);
+                }
+            }
+        }
+        psFree (words);
+    }
+
+    // load the list from the supplied text file
+    if (list && strlen(list) > 0 && (Narg = psArgumentGet (*argc, argv, list))) {
+        int nItems;
+        char line[1024]; // XXX limits the list lines to 1024 chars
+        char word[1024];
+        char *filename;
+
+        psArgumentRemove (Narg, argc, argv);
+        FILE *f = fopen (argv[Narg], "r");
+        if (f == NULL) {
+            psAbort("unable to open specified list file");
+        }
+        while (fgets (line, 1024, f) != NULL) {
+            nItems = sscanf (line, "%s", word);
+            switch (nItems) {
+            case 0:
+                break;
+            case 1:
+                filename = psStringCopy (word);
+                psArrayAdd (input, 16, filename);
+                psFree (filename);
+                break;
+            default:
+                // rigid format, no comments allowed?
+                psAbort("error parsing input list file");
+                break;
+            }
+        }
+        psArgumentRemove (Narg, argc, argv);
+    }
+
+    return input;
+}
+
+bool pmConfigFileSetsMD(psMetadata *metadata, int *argc, char **argv, const char *name,
+                        const char *file, const char *list)
+{
+    PS_ASSERT_PTR_NON_NULL(metadata, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    psErrorClear();   // pmConfigFileSets may or may not call psError, so
+    // if files->n == 0 we'll want to call psError(..., false, ...)
+    psArray *files = pmConfigFileSets(argc, argv, file, list);
+    if (files->n == 0) {
+        // psError(PS_ERR_IO, false, "pmConfigFileSets failed to find %s in the argument list", name);
+        psFree (files);
+        return false;
+    }
+
+    psMetadataAddPtr(metadata, PS_LIST_TAIL, name,  PS_DATA_ARRAY, "", files);
+    psFree (files);
+    return true;
+}
+
+// convert the supplied name, create a new output psString
+psString pmConfigConvertFilename(const char *filename, const pmConfig *config, bool create)
+{
+    PS_ASSERT_STRING_NON_EMPTY(filename, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    // strip file:// from front of name
+    if (!strncasecmp(filename, "file:", strlen("file:"))) {
+        psString newName = psStringCopy(filename);
+
+        char *point = newName + strlen("file:");
+        while (*point == '/')
+            point ++;
+        char *tmpName = NULL;
+        psStringAppend (&tmpName, "/%s", point);
+        psFree (newName);
+        newName = tmpName;
+
+        if (!create && access(newName, R_OK) != 0) {
+            psError(PS_ERR_IO, true, "Unable to access file %s", newName);
+            psFree(newName);
+            return NULL;
+        }
+
+        return newName;
+    }
+
+    // replace path://PATH with matched datapath
+    if (!strncasecmp(filename, "path://", strlen("path://"))) {
+        PS_ASSERT_METADATA_NON_NULL(config->site, NULL);
+
+        psString newName = psStringCopy(filename);
+
+        // filename should be of the form: path://PATH/rest/of/file
+        // replace PATH with matching name from config->site:DATAPATH
+        psMetadata *datapath = psMetadataLookupPtr (NULL, config->site, "DATAPATH");
+        if (datapath == NULL) {
+            psError(PS_ERR_UNKNOWN, true, "DATAPATH is not defined in config.site");
+            psFree (newName);
+            return NULL;
+        }
+
+        char *point = newName + strlen("path://");
+        char *mark = strchr (point, '/');
+        if (mark == NULL) {
+            psError(PS_ERR_UNKNOWN, true, "syntax error in PATH-style name %s", newName);
+            psFree (newName);
+            return false;
+        }
+
+        psString path = psStringNCopy (point, mark - point);
+        char *realpath = psMetadataLookupStr (NULL, datapath, path);
+        if (realpath == NULL) {
+            psError(PS_ERR_UNKNOWN, true,
+                    "path (%s) not defined in config.site:DATAPATH for PATH-style name %s",
+                    path, newName);
+            psFree(newName);
+            psFree(path);
+            return false;
+        }
+        psFree(path);
+
+        char *tmpName = NULL;
+        psStringAppend(&tmpName, "%s/%s", realpath, mark + 1);
+        psFree(newName);
+        newName = tmpName;
+
+        if (!create && access(newName, R_OK) != 0) {
+            psError(PS_ERR_IO, true, "Unable to access file %s", newName);
+            psFree(newName);
+            return NULL;
+        }
+        return newName;
+    }
+
+    // substitute neb://name with matched nebulous name
+    if (!strncasecmp(filename, "neb://", strlen("neb://"))) {
+        #ifdef HAVE_NEBCLIENT
+
+        bool status = false;
+        psString neb_server = NULL;
+
+        // check the env first
+        neb_server = getenv("NEB_SERVER");
+
+        // if env isn't set, check the config system
+        if (!neb_server) {
+            neb_server = psMetadataLookupStr(&status, config->site, "NEB_SERVER");
+            if (!status) {
+                psError(PM_ERR_CONFIG, true, "failed to lookup config value for NEB_SERVER.");
+                return NULL;
+            }
+        }
+
+        if (!neb_server) {
+            psError(PM_ERR_CONFIG, true, "Could not determine nebulous server URI.");
+            return NULL;
+        }
+
+        nebServer *server = nebServerAlloc(neb_server);
+        if (!server) {
+            psError(PM_ERR_SYS, true, "failed to create a nebServer object: %s", nebErr(server));
+            nebServerFree(server);
+            return NULL;
+        }
+
+        // ignore the leading neb://
+        char *cleanFilename = (char *)filename + strlen("neb://");
+
+        char *nebfile = NULL;
+        if (create) {
+            nebfile = nebCreate(server, cleanFilename, NULL, NULL);
+            if (!nebfile) {
+                psError(PM_ERR_SYS, true, "failed to create a new nebulous key: %s", nebErr(server));
+                nebServerFree(server);
+                return NULL;
+            }
+        } else {
+            nebfile = nebFind(server, cleanFilename);
+            if (!nebfile) {
+                psError(PM_ERR_SYS, true, "failed to find nebulous key: %s", nebErr(server));
+                nebServerFree(server);
+                return NULL;
+            }
+        }
+
+        // convert nebfile into a psString
+        psString path = psStringCopy(nebfile);
+        nebFree(nebfile);
+        nebServerFree(server);
+
+        return path;
+
+        #else // ifdef HAVE_NEBCLIENT
+
+        psError(PM_ERR_PROG, true, "psModules was compiled without nebulous support.");
+        return NULL;
+        #endif // ifdef HAVE_NEBCLIENT
+
+    }
+
+    // if we go this far, do nothing
+    return psStringCopy(filename);
+}
+
+psMetadata *pmConfigFileRule(const pmConfig *config, const psMetadata *camera, const char *name)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_METADATA_NON_NULL(camera, NULL);
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    psMetadataItem *item = psMetadataLookup(camera, "FILERULES"); // Item with the file rule of interest
+    if (!item) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find FILERULES in the camera configuration.");
+        return NULL;
+    }
+
+    if (!metadataItemReadFile(item, "file rules ")) {
+        psError(PM_ERR_CONFIG, false, "Unable to read file rules for camera.");
+        return NULL;
+    }
+
+    assert(item->type == PS_DATA_METADATA);
+    psMetadata *filerules = item->data.md; // File rules from the camera configuration
+
+    // select the name from the FILERULES
+    // check for alias name (type == STR, name is aliased name)
+    bool mdok;                          // Status of MD lookup
+    const char *realname = psMetadataLookupStr(&mdok, filerules, name); // Name of file rule to look up
+    if (!realname || strlen(realname) == 0) {
+        realname = name;
+    }
+
+    return psMetadataLookupMetadata(NULL, filerules, realname);
+}
+
Index: /branches/eam_branch_20071023/psModules/src/config/pmConfig.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmConfig.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmConfig.h	(revision 15358)
@@ -0,0 +1,182 @@
+/*  @file pmConfig.h
+ *  @brief Configuration functions
+ *
+ *  @author Paul Price, IfA
+ *  @author Eugene Magnier, IfA
+ *
+ *  @version $Revision: 1.31 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-10-04 20:15:48 $
+ *  Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONFIG_H
+#define PM_CONFIG_H
+
+/// @addtogroup Config Configuration System
+/// @{
+
+/// Sources for recipes.
+///
+/// Defines what recipe sources have been read.  This allows us to read recipes from different sources as they
+/// become available.  For example, we may not have access to the camera configuration until we have read a
+/// FITS file.  We allow symbolic links, which means the user can specify on the command-line the name of a
+/// recipe that's defined elsewhere, instead of typing the entire filename.  This structure is private to
+/// psModules --- there is no need for the user to know about it.
+typedef enum {
+    PM_RECIPE_SOURCE_NONE        = 0x00, ///< None yet
+    PM_RECIPE_SOURCE_SITE        = 0x01, ///< Site configuration
+    PM_RECIPE_SOURCE_CAMERA      = 0x02, ///< Camera configuration
+    PM_RECIPE_SOURCE_CL          = 0x04, ///< Command-line
+    PM_RECIPE_SOURCE_SYMBOLIC    = 0x14, ///< Symbolic link, specified on command-line
+    PM_RECIPE_SOURCE_ALL         = 0xff  ///< All sources
+} pmRecipeSource;
+
+/// Configuration information
+///
+/// This structure stores the configuration information: the site, camera and recipe configuration, the
+/// command-line arguments, the pmFPAfiles used, and the database handle.
+typedef struct
+{
+    psMetadata *site;                   ///< Site configuration
+    psMetadata *camera;                 ///< Camera specification
+    psString cameraName;                ///< Camera name
+    psMetadata *format;                 ///< Camera format description
+    psString formatName;                ///< Camera format name
+    psMetadata *recipes;                ///< Recipes for processing
+    psMetadata *arguments;              ///< Processed command-line arguments
+    psMetadata *files;                  ///< pmFPAfiles used for analysis
+    psDB *database;                     ///< Database handle
+    const char *defaultRecipe;          ///< name of top-level recipe for this program
+    // Private members
+    pmRecipeSource recipesRead;         ///< Which recipe sources have been read
+    psMetadata *recipeSymbols;          ///< Where each recipe came from
+
+    // dropping the argc, argv info from pmConfig
+    # if (0)
+    int *argc;                          ///< Number of command-line arguments
+    char **argv;                        ///< Command-line arguments (raw version)
+    # endif
+}
+pmConfig;
+
+/// Allocator for pmConfig
+pmConfig *pmConfigAlloc();
+
+/// Set static configuration information
+///
+/// The search path for the configuration files is a local static variable, set by this function.
+void pmConfigSet(const char *path ///< Search paths for configuration files; colon-delimited directories
+                );
+
+/// Free static memory used in the configuration system
+void pmConfigDone(void);
+
+/// Read configuration information from the command line.
+///
+/// pmConfigRead loads the site configuration (the file name is specified by "-site SITE_FILE" on the
+/// command-line, the PS_SITE environment variable, or it is $HOME/.ipprc).  The configuration search path is
+/// set. The camera configuration is loaded if it is specified on the command line ("-camera
+/// CAMERA_FILE"). Recipes specified on the command line ("-recipe RECIPE_NAME RECIPE_SOURCE") are also
+/// loaded.  These command-line arguments are removed from from the command-line, to simplify parsing.  The
+/// psLib log, trace and time setups are also performed if specified in the site configuration.
+pmConfig *pmConfigRead(int *argc,       ///< Number of command-line arguments
+                       char **argv, ///< Array of command-line arguments
+                       const char *defaultRecipe ///< name of top-level recipe for this program
+                      );
+
+/// Read a configuration file
+///
+/// Read a metadata configuration file into the supplied metadata.  Produce an error and return false if
+/// there's a problem.
+bool pmConfigFileRead(psMetadata **config, ///< Config to output
+                      const char *name, ///< Name of file
+                      const char *description ///< Description of file
+                     );
+
+/// Validate a header against the camera format
+///
+/// Given a FITS header (the PHU header), check it against the RULE metadata contained within the camera
+/// format; return found = true if it matches. return false on serious errors
+bool pmConfigValidateCameraFormat(bool *valid,
+                                  const psMetadata *cameraFormat, ///< Camera format containing the RULE
+                                  const psMetadata *header // FITS header for the PHU
+                                 );
+
+/// Determine the camera format (and camera if unknown) from examining the header
+///
+/// Given a FITS header, check it against all known cameras (unless we already know which camera, from
+/// pmConfigRead) and all known formats for those cameras in order to identify which is appropriate.  The
+/// first matching format is accepted; further matches produce warnings.  The accepted camera is saved in the
+/// configuration.  The accepted format is returned.
+psMetadata *pmConfigCameraFormatFromHeader(pmConfig *config, ///< The configuration
+                                           const psMetadata *header, ///< The FITS header
+                                           bool readRecipes ///< optionally read the recipes as well as the format
+    );
+
+/// Return the camera configuration specified by name
+///
+/// Given a camera name, returns the camera configuration metadata.
+psMetadata *pmConfigCameraByName(pmConfig *config, ///< The configuration
+                                 const char *cameraName ///< The camera name header
+                                );
+
+/// Setup the database
+///
+/// Initialise the database connection using the DBSERVER, DBNAME, DBUSER, DBPASSWORD values provided in the
+/// site configuration.  Stores the database handle in the configuration, and also returns it.
+psDB *pmConfigDB(pmConfig *config       ///< Configuration
+                );
+
+/// Make the supplied header conform to the nominated camera format.
+///
+/// Given a FITS header, make it conform to the RULE in the specified camera format.  This is useful for
+/// switching between formats, or generating fake data that must be recognised by
+/// pmConfigCameraFormatFromHeader.
+bool pmConfigConformHeader(psMetadata *header, ///< Header to conform
+                           const psMetadata *format ///< Camera format
+                          );
+
+/// Read the command-line for files (or a text file containing a list of files)
+///
+/// Given the 'file' and 'list' arguments (e.g., "-file" and "-list"), find the arguments associated with
+/// these words and interpret them as lists of files.  Return an array of the resulting filenames.
+psArray *pmConfigFileSets(int *argc,    ///< Number of arguments (I/O)
+                          char **argv,  ///< Array of arguments
+                          const char *file, ///< CL argument specifying a filename
+                          const char *list ///< CL argument specifying a text file with a list of filenames
+                         );
+
+/// Stuff associated files from the command-line into a metadata
+///
+/// Calls pmConfigFileSets to parse the command line for filenames (or a list which provides filenames), and
+/// stuffs the array of filenames into the metadata under "name".
+bool pmConfigFileSetsMD(psMetadata *metadata, ///< Metadata into which to stuff the array
+                        int *argc,    ///< Number of arguments (I/O)
+                        char **argv,  ///< Array of arguments
+                        const char *name, ///< Name for array in the metadata
+                        const char *file, ///< CL argument specifying a filename
+                        const char *list ///< CL argument specifying a text file with a list of filenames
+                       );
+
+/// Convert the supplied name, create a new output psString
+psString pmConfigConvertFilename(
+    const char *filename,               ///< file path/URI
+    const pmConfig *config,             ///< configuration
+    bool create                         ///< create the file if it doesn't exist
+);
+
+/// Set whether all config parameters are read on startup
+bool pmConfigReadParamsSet(bool newReadCameraConfig // Desired mode for camera configuration reading
+                          );
+
+/// Get the file rule of interest
+///
+/// Look up the name of the set of file rules to use, get that set from the site configuration, and return the
+/// appropriate rule from the set.
+psMetadata *pmConfigFileRule(const pmConfig *config, ///< Configuration
+                             const psMetadata *camera, ///< Camera configuration of interest
+                             const char *name ///< Name of rule to read
+    );
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/config/pmConfigCamera.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmConfigCamera.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmConfigCamera.c	(revision 15358)
@@ -0,0 +1,875 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>            /* for strn?casecmp */
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmVersion.h"
+#include "pmConcepts.h"
+#include "pmConfigCamera.h"
+
+#define TABLE_OF_CONTENTS "CONTENTS"    // Name for camera format metadata containing the contents
+#define CHIP_TYPES "CHIPS"              // Name for camera format metadata containing the chip types
+#define CELL_TYPES "CELLS"              // Name for camera format metadata containing the cell types
+
+// local helper functions defined below
+static void removeCellConceptsSources(psMetadata *source);
+static void removeChipConceptsSources(psMetadata *source);
+
+// Generate the skycell version of a named camera configuration
+bool pmConfigCameraSkycellVersion(psMetadata *site, // The site configuration
+                                  const char *name // Name of the un-mosaicked camera
+                                  )
+{
+    PS_ASSERT_METADATA_NON_NULL(site, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *cameras = psMetadataLookupMetadata(&mdok, site, "CAMERAS"); // List of cameras
+    if (!mdok || !cameras) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the site configuration.\n");
+        return false;
+    }
+    if (!pmConfigGenerateSkycellVersion(cameras, cameras, name, site)) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to build skycell camera description for %s\n", name);
+        return false;
+    }
+    return true;
+}
+
+
+bool pmConfigCameraSkycellVersionsAll(psMetadata *site)
+{
+    PS_ASSERT_METADATA_NON_NULL(site, false);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *cameras = psMetadataLookupMetadata(&mdok, site, "CAMERAS"); // List of cameras
+    if (!mdok || !cameras) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the site configuration.\n");
+        return false;
+    }
+
+    psMetadataIterator *camerasIter = psMetadataIteratorAlloc(cameras, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *camerasItem = NULL; // Item from iteration
+    psMetadata *new = psMetadataAlloc();// New cameras to add
+    while ((camerasItem = psMetadataGetAndIncrement(camerasIter))) {
+        assert(camerasItem->type == PS_DATA_METADATA); // Only metadata are allowed here!
+        if (!pmConfigGenerateSkycellVersion(cameras, new, camerasItem->name, site)) {
+            psError(PS_ERR_UNKNOWN, true, "Failed to build skycell camera description for %s\n",
+                    camerasItem->name);
+            return false;
+        }
+    }
+    psFree(camerasIter);
+
+    // Now put the new cameras at the top of the list of cameras, so they get recognised first
+    // Note: going from the top, and putting everything to the top as we get there, so that the last one on
+    // goes to the top.  This preserves the original order of the cameras, putting the skycell versions
+    // before the originals.
+    camerasIter = psMetadataIteratorAlloc(new, PS_LIST_HEAD, NULL); // Iterator
+    while ((camerasItem = psMetadataGetAndIncrement(camerasIter))) {
+        psMetadataAddItem(cameras, camerasItem, PS_LIST_HEAD, 0);
+    }
+    psFree(camerasIter);
+    psFree(new);
+
+    return true;
+}
+
+// Don't update these skycell concepts; last one MUST be 0 (i.e., NULL).
+const static char *skycellConceptsCell[] = { "CELL.BIASSEC", "CELL.TRIMSEC", "CELL.READDIR", "CELL.XPARITY",
+                                             "CELL.YPARITY", "CELL.XWINDOW", "CELL.YWINDOW", "CELL.X0",
+                                             "CELL.Y0", "CELL.XSIZE", "CELL.YSIZE", 0 };
+const static char *skycellConceptsChip[] = { "CHIP.XPARITY", "CHIP.YPARITY", "CHIP.XSIZE", "CHIP.YSIZE", 0 };
+const static char *skycellConceptsFPA[] = { 0 };
+
+// What do we call the skycell concept in the FITS header?
+static const char *skycellConceptName(const char *name, // Name of concept
+                                      const char **concepts, // List of concepts NOT to update
+                                      const psMetadata *site // Site configuration
+                                      )
+{
+    for (int i = 0; concepts[i]; i++) {
+        if (strcmp(name, concepts[i]) == 0) {
+            return NULL;
+        }
+    }
+
+    if (!site) {
+        return name;
+    }
+    bool mdok;                          // Status of MD lookup
+    psMetadata *skycells = psMetadataLookupMetadata(&mdok, site, "SKYCELLS"); // Skycell concept headers
+    if (!skycells) {
+        return name;
+    }
+    const char *keyword = psMetadataLookupStr(&mdok, skycells, name); // Keyword to use for this concept
+    if (!mdok || !keyword || strlen(keyword) == 0) {
+        return name;
+    }
+    return keyword;
+}
+
+
+// Generate a skycell version of a camera configuration
+bool pmConfigGenerateSkycellVersion(psMetadata *oldCameras, // Old list of camera configurations
+                                    psMetadata *newCameras, // New list of camera configurations
+                                    const char *name, // Name of original camera configuration
+                                    const psMetadata *site // Site configuration
+                                    )
+{
+    assert(oldCameras);
+    assert(newCameras);
+    assert(name);
+
+    // See if the old one is there
+    psMetadata *camera = psMetadataLookupMetadata(NULL, oldCameras, name); // The camera configuration
+    if (!camera) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Can't find camera to be skycelled in camera list.");
+        return false;
+    }
+
+    // See if the new one is already there
+    psString newName = NULL;       // Name of skycelled camera
+    psStringAppend(&newName, "_%s-SKYCELL", name);
+    if (psMetadataLookup(oldCameras, newName)) {
+        return true;
+    }
+
+    psMetadata *new = psMetadataCopy(NULL, camera); // Copy of the camera description
+
+    // Fix the FPA description to contain a single chip with single cell
+    {
+        psMetadata *fpa = psMetadataAlloc();// The FPA description
+        psMetadataAddStr(fpa, PS_LIST_HEAD, "SkyChip", 0, "Single chip with single cell", "SkyCell");
+        psMetadataAddMetadata(new, PS_LIST_TAIL, "FPA", PS_META_REPLACE, "Description of FPA hierarchy", fpa);
+        psFree(fpa);
+    }
+
+    // Clear out the formats, replace them with the One True Format
+    psMetadata *formats = psMetadataLookupMetadata(NULL, new, "FORMATS"); // The list of formats
+    if (!formats) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Can't find FORMATS within camera configuration.");
+        psFree(new);
+        return false;
+    }
+    while (psListLength(formats->list) > 0) {
+        psMetadataRemoveIndex(formats, PS_LIST_HEAD);
+    }
+    psMetadata *format = psMetadataAlloc(); // The One True Format
+
+    {
+        psMetadata *rule = psMetadataAlloc(); // The RULE --- how to recognise the camera
+        psMetadataAddStr(rule, PS_LIST_TAIL, "PSCAMERA", 0, "Camera name", name);
+        psMetadataAddStr(rule, PS_LIST_TAIL, "PSFORMAT", 0, "Camera format", "SKYCELL");
+        psString version = psModulesVersion();
+        psMetadataAddStr(rule, PS_LIST_TAIL, "ORIGIN", 0, "File origin", version);
+        psFree(version);
+        psMetadataAddMetadata(format, PS_LIST_TAIL, "RULE", 0, "How to recognise this type of file", rule);
+        psFree(rule);
+    }
+
+    {
+        psMetadata *file = psMetadataAlloc(); // The FILE --- how to read the data
+        psMetadataAddStr(file, PS_LIST_TAIL, "PHU", 0, "What level the FITS file represents", "FPA");
+        psMetadataAddStr(file, PS_LIST_TAIL, "EXTENSIONS", 0, "What level the extensions represent", "NONE");
+        psMetadataAddStr(file, PS_LIST_TAIL, "FPA.NAME", 0, "PHU keyword for unique identifier", "FPA.NAME");
+        psMetadataAddMetadata(format, PS_LIST_TAIL, "FILE", 0, "How to read this type of file", file);
+        psFree(file);
+    }
+
+    psMetadataAddStr(format, PS_LIST_TAIL, "CONTENTS", 0, "What's in this type of file",
+                     "SkyChip:SkyCell:_skycell");
+
+    {
+        psMetadata *cells = psMetadataAlloc(); // The CELLS --- how to read the cells
+        psMetadata *skycell = psMetadataAlloc(); // How to read the skycell
+        psMetadataAddStr(skycell, PS_LIST_TAIL, "CELL.TRIMSEC", 0, "Trim section", "CELL.TRIMSEC");
+        psMetadataAddStr(skycell, PS_LIST_TAIL, "CELL.BIASSEC", 0, "Bias section", "CELL.BIASSEC");
+        psMetadataAddStr(skycell, PS_LIST_TAIL, "CELL.TRIMSEC.SOURCE", 0, "Source for trim section",
+                         "HEADER");
+        psMetadataAddStr(skycell, PS_LIST_TAIL, "CELL.BIASSEC.SOURCE", 0, "Source for bias section",
+                         "HEADER");
+        psMetadataAddMetadata(cells, PS_LIST_TAIL, "_skycell", 0, "Skycell specification", skycell);
+        psFree(skycell);
+        psMetadataAddMetadata(format, PS_LIST_TAIL, "CELLS", 0, "How to read the cells", cells);
+        psFree(cells);
+    }
+
+    // Stuffing all concepts into the header, by their PS concept name (e.g., "FPA.AIRMASS").
+    // (HIERARCH will take care of the long names, implemented in psLib.)
+    // Some people may not like this, but it's quick and easy and will do for now.
+    // An alternative may be provided later.
+    {
+        psMetadata *translation = psMetadataAlloc(); // The TRANSLATION --- how to read the FITS headers
+
+        psList *concepts;               // List of concepts for each level
+        psListIterator *iter;           // Iterator for concepts
+        psString name;                  // Concept name, from iteration
+
+        concepts = pmConceptsList(PM_FPA_LEVEL_FPA); // FPA-level concepts
+        iter = psListIteratorAlloc(concepts, PS_LIST_HEAD, false);
+        while ((name = psListGetAndIncrement(iter))) {
+            const char *new = skycellConceptName(name, skycellConceptsFPA, site); // Name for skycell
+            if (new) {
+                psMetadataAddStr(translation, PS_LIST_TAIL, name, 0, NULL, new);
+            }
+        }
+        psFree(iter);
+        psFree(concepts);
+
+        concepts = pmConceptsList(PM_FPA_LEVEL_CHIP);
+        iter = psListIteratorAlloc(concepts, PS_LIST_HEAD, false);
+        while ((name = psListGetAndIncrement(iter))) {
+            const char *new = skycellConceptName(name, skycellConceptsChip, site); // Name for skycell
+            if (new) {
+                psMetadataAddStr(translation, PS_LIST_TAIL, name, 0, NULL, new);
+            }
+        }
+        psFree(iter);
+        psFree(concepts);
+
+        concepts = pmConceptsList(PM_FPA_LEVEL_CELL);
+        iter = psListIteratorAlloc(concepts, PS_LIST_HEAD, false);
+        while ((name = psListGetAndIncrement(iter))) {
+            const char *new = skycellConceptName(name, skycellConceptsCell, site); // Name for skycell
+            if (new) {
+                psMetadataAddStr(translation, PS_LIST_TAIL, name, 0, NULL, new);
+            }
+        }
+        psFree(iter);
+        psFree(concepts);
+
+        psMetadataAddStr(translation, PS_LIST_TAIL, "CELL.XSIZE", 0, NULL, "NAXIS1");
+        psMetadataAddStr(translation, PS_LIST_TAIL, "CELL.YSIZE", 0, NULL, "NAXIS2");
+        psMetadataAddStr(translation, PS_LIST_TAIL, "CELL.XWINDOW", 0, NULL, "NAXIS1");
+        psMetadataAddStr(translation, PS_LIST_TAIL, "CELL.YWINDOW", 0, NULL, "NAXIS2");
+        psMetadataAddStr(translation, PS_LIST_TAIL, "CHIP.XSIZE", 0, NULL, "NAXIS1");
+        psMetadataAddStr(translation, PS_LIST_TAIL, "CHIP.YSIZE", 0, NULL, "NAXIS2");
+
+        psMetadataAddMetadata(format, PS_LIST_TAIL, "TRANSLATION", 0, "How to translate the FITS headers",
+                              translation);
+        psFree(translation);
+    }
+
+    {
+        psMetadata *defaults = psMetadataAlloc(); // Default values for concepts
+
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.XPARITY", 0, NULL, 1);
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.YPARITY", 0, NULL, 1);
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.XPARITY", 0, NULL, 1);
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.YPARITY", 0, NULL, 1);
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.Y0", 0, NULL, 0);
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.X0", 0, NULL, 0);
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.Y0", 0, NULL, 0);
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.X0", 0, NULL, 0);
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.READDIR", 0, "Read direction (rows)", 1);
+
+        psMetadataAddMetadata(format, PS_LIST_TAIL, "DEFAULTS", 0, "Default values for concepts", defaults);
+        psFree(defaults);
+
+    }
+
+    {
+        psMetadata *database = psMetadataAlloc(); // Database values for concepts
+        psMetadataAddMetadata(format, PS_LIST_TAIL, "DATABASE", 0, "Database values for concepts", database);
+        psFree(database);
+    }
+
+    {
+        psMetadata *conceptFormats = psMetadataAlloc(); // Format peculiarities for various concepts
+        // These are the only essential formats
+        psMetadataAddStr(conceptFormats, PS_LIST_TAIL, "FPA.RA", 0, "Units for RA", "HOURS");
+        psMetadataAddStr(conceptFormats, PS_LIST_TAIL, "FPA.DEC", 0, "Units for RA", "DEGREES");
+        psMetadataAddStr(conceptFormats, PS_LIST_TAIL, "FPA.TIME", 0, "Format for time", "MJD");
+        psMetadataAddStr(conceptFormats, PS_LIST_TAIL, "CELL.TIME", 0, "Format for time", "MJD");
+
+        psMetadataAddMetadata(format, PS_LIST_TAIL, "FORMATS", 0, "Formats for various concepts",
+                              conceptFormats);
+        psFree(conceptFormats);
+    }
+
+    psMetadataAddMetadata(formats, PS_LIST_TAIL, "SKYCELL", 0, "The One True Format for skycells", format);
+    psFree(format);
+
+    // New camera MUST go to the head of the metadata, so that it will be recognised first
+    // The old camera doesn't contain the PSCAMERA and PSFORMAT headers, so it will match anything!
+    psMetadataAddMetadata(newCameras, PS_LIST_HEAD, newName, PS_META_REPLACE,
+                          "Automatically generated", new);
+    psFree(newName);
+    psFree(new);
+
+    return true;
+}
+
+// Generate the Chip and FPA mosaicked version of a named camera configuration
+bool pmConfigCameraMosaickedVersions(psMetadata *site, // The site configuration
+                                     const char *name // Name of the un-mosaicked camera
+                                    )
+{
+    PS_ASSERT_METADATA_NON_NULL(site, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *cameras = psMetadataLookupMetadata(&mdok, site, "CAMERAS"); // List of cameras
+    if (!mdok || !cameras) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the site configuration.\n");
+        return false;
+    }
+    if (!pmConfigGenerateMosaickedVersion(cameras, cameras, name, PM_FPA_LEVEL_CHIP)) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to build Chip mosaic camera description for %s\n", name);
+        return false;
+    }
+    if (!pmConfigGenerateMosaickedVersion(cameras, cameras, name, PM_FPA_LEVEL_FPA)) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to build FPA mosaic camera description for %s\n", name);
+        return false;
+    }
+    return true;
+}
+
+// the operation putting the new entries first is now implemented in pmConfigGenerateMosaickedVersion
+// Generate the Chip and FPA mosaicked version of a named camera configuration
+bool pmConfigCameraMosaickedVersionsAll(psMetadata *site)
+{
+    PS_ASSERT_METADATA_NON_NULL(site, false);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *cameras = psMetadataLookupMetadata(&mdok, site, "CAMERAS"); // List of cameras
+    if (!mdok || !cameras) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find CAMERAS in the site configuration.\n");
+        return false;
+    }
+
+    psMetadataIterator *camerasIter = psMetadataIteratorAlloc(cameras, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *camerasItem = NULL; // Item from iteration
+    psMetadata *new = psMetadataAlloc();// New cameras to add
+    while ((camerasItem = psMetadataGetAndIncrement(camerasIter))) {
+        assert(camerasItem->type == PS_DATA_METADATA); // Only metadata are allowed here!
+        if (!pmConfigGenerateMosaickedVersion(cameras, new, camerasItem->name, PM_FPA_LEVEL_CHIP)) {
+            psError(PS_ERR_UNKNOWN, true, "Failed to build Chip mosaic camera description for %s\n", camerasItem->name);
+            return false;
+        }
+        if (!pmConfigGenerateMosaickedVersion(cameras, new, camerasItem->name, PM_FPA_LEVEL_FPA)) {
+            psError(PS_ERR_UNKNOWN, true, "Failed to build FPA mosaic camera description for %s\n", camerasItem->name);
+            return false;
+        }
+    }
+    psFree(camerasIter);
+
+    // Now put the new cameras at the top of the list of cameras, so they get recognised first
+    // Note: going from the top, and putting everything to the top as we get there, so that the last one on
+    // goes to the top.  This preserves the original order of the cameras, putting the mosaicked versions
+    // before the originals.
+    camerasIter = psMetadataIteratorAlloc(new, PS_LIST_HEAD, NULL); // Iterator
+    while ((camerasItem = psMetadataGetAndIncrement(camerasIter))) {
+        psMetadataAddItem(cameras, camerasItem, PS_LIST_HEAD, 0);
+    }
+    psFree(camerasIter);
+    psFree(new);
+
+    return true;
+}
+
+// Generate a mosaicked version of a camera configuration
+bool pmConfigGenerateMosaickedVersion(psMetadata *oldCameras, // Old list of camera configurations
+                                      psMetadata *newCameras, // New list of camera configurations
+                                      const char *name, // Name of original camera configuration
+                                      pmFPALevel mosaicLevel // Level to which we are mosaicking
+    )
+{
+    assert(oldCameras);
+    assert(newCameras);
+    assert(name);
+    assert(mosaicLevel == PM_FPA_LEVEL_CHIP || mosaicLevel == PM_FPA_LEVEL_FPA);
+
+    // Don't generate a mosaicked version of a skycell
+    if (name[0] == '_' && strlen(name) > 9 && strcmp(name + strlen(name) - 8, "-SKYCELL") == 0) {
+        return true;
+    }
+
+    // See if the old one is there
+    psMetadata *camera = psMetadataLookupMetadata(NULL, oldCameras, name); // The camera configuration
+    if (!camera) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Can't find camera to be mosaicked in camera list.");
+        return false;
+    }
+
+    // See if the new one is already there
+    psString newName = NULL;       // Name of mosaicked camera
+    psStringAppend(&newName, "_%s-%s", name, mosaicLevel == PM_FPA_LEVEL_CHIP ? "CHIP" : "FPA");
+    if (psMetadataLookup(oldCameras, newName)) {
+        return true;
+    }
+
+    psMetadata *new = psMetadataCopy(NULL, camera); // Copy of the camera description
+    bool mdok;                          // Status of MD lookups
+
+    // ** Fix up the contents of the FPA description to match the mosaicked camera **
+    // select the FPA description
+    psMetadata *fpa = psMetadataLookupMetadata(NULL, new, "FPA"); // FPA in the camera configuration
+    if (!fpa) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Can't find FPA within camera configuration.");
+        psFree(new);
+        return false;
+    }
+    switch (mosaicLevel) {
+        // For CHIP mosaic, replace the contents of each chip with a single cell
+      case PM_FPA_LEVEL_CHIP: {
+          psMetadataIterator *fpaIter = psMetadataIteratorAlloc(fpa, PS_LIST_HEAD, NULL); // Iterator
+          psMetadataItem *fpaItem = NULL;     // Item from iteration
+          while ((fpaItem = psMetadataGetAndIncrement(fpaIter))) {
+              if (fpaItem->type != PS_DATA_STRING) {
+                  psError(PS_ERR_UNKNOWN, true, "Element %s within FPA in camera configuration is not of type STR.",
+                            fpaItem->name);
+                  psFree(new);
+                  return false;
+              }
+
+              psFree(fpaItem->data.str);
+              fpaItem->data.str = psStringCopy("MosaickedCell");
+              psFree(fpaItem->comment);
+              fpaItem->comment = psStringCopy("Mosaicked cell; automatically generated");
+          }
+          psFree(fpaIter);
+          break;
+      }
+        // For FPA mosaic, replace the contents of the FPA with a single chip containing a single cell
+      case PM_FPA_LEVEL_FPA: {
+          while (psListLength(fpa->list) > 0) {
+              psMetadataRemoveIndex(fpa, PS_LIST_TAIL);
+          }
+
+          psMetadataAddStr(fpa, PS_LIST_HEAD, "MosaickedChip", 0,
+                           "Mosaicked chip with mosaicked cell; automatically generated",
+                           "MosaickedCell");
+          break;
+      }
+    default:
+        psAbort("Should never get here.\n");
+    }
+
+    // ** Update the camera formats : add a new (mosaicked) format for each existing camera format **
+    // select the list of all camera formats
+    psMetadata *formats = psMetadataLookupMetadata(NULL, new, "FORMATS"); // FORMATS in the configuration
+    assert(formats);            // It had better be there --- we've already read them in
+    // loop over each of the formats
+    psMetadataIterator *formatsIter = psMetadataIteratorAlloc(formats, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *formatsItem = NULL; // Item from iteration
+    while ((formatsItem = psMetadataGetAndIncrement(formatsIter))) {
+        assert(formatsItem->type == PS_DATA_METADATA); // We should have read it by now!
+        psMetadata *format = formatsItem->data.V; // The camera format
+
+        // Add a new RULE which uniquely describes the mosaicked format.  this is needed so
+        // that when a mosaic is written to a FITS file, it can be recognised again when read.
+        psMetadata *rule = psMetadataLookupMetadata(NULL, format, "RULE"); // Way to identify format from PHU
+        if (!rule) {
+            // a camera format without a rule is not allowed.
+            psError(PS_ERR_UNKNOWN, false, "Camera format %s has no RULE", formatsItem->name);
+            return false;
+        }
+
+        // the new rule is supplemented by the mosaicLevel
+        switch (mosaicLevel) {
+        case PM_FPA_LEVEL_CHIP:
+            psMetadataAddStr(rule, PS_LIST_TAIL, "PSMOSAIC", 0, "Mosaicked level", "CHIP");
+            break;
+        case PM_FPA_LEVEL_FPA:
+            psMetadataAddStr(rule, PS_LIST_TAIL, "PSMOSAIC", 0, "Mosaicked level", "FPA");
+            break;
+        default:
+            psAbort("Should never get here.\n");
+        }
+
+        // Fix the FILE information: need to fix the levels for the PHU and EXTENSIONS.
+        // both of these elements are required in the format; we raise an error if they are not found
+        // If EXTENSIONS is NONE, then we need to change the CONTENT specifier to point to the chip name.
+        psMetadata *file = psMetadataLookupMetadata(NULL, format, "FILE"); // File information
+        if (!file) {
+            psError(PS_ERR_UNKNOWN, false, "Camera format %s has no FILE", formatsItem->name);
+            return false;
+        }
+        psMetadataItem *phuItem = psMetadataLookup(file, "PHU"); // PHU level
+        if (!phuItem || phuItem->type != PS_DATA_STRING) {
+            psError(PS_ERR_UNKNOWN, false, "Camera format %s is missing PHU in the FILE information", formatsItem->name);
+            return false;
+        }
+        psMetadataItem *extensionsItem = psMetadataLookup(file, "EXTENSIONS"); // Extensions level
+        if (!extensionsItem || extensionsItem->type != PS_DATA_STRING) {
+            psError(PS_ERR_UNKNOWN, false, "Camera format %s is missing EXTENSIONS in the FILE information", formatsItem->name);
+            return false;
+        }
+
+        // mosaicLevel == CHIP:
+        // Case    PHU     EXTENSIONS     Modifications
+        // ====    ===     ==========     ===========
+        // 1.      FPA     CHIP           NONE
+        // 2.      FPA     CELL           EXT->CHIP
+        // 3.      FPA     NONE           NONE
+        // 4.      CHIP    CELL           EXT->NONE
+        // 5.      CHIP    NONE           NONE
+        // 6.      CELL    NONE           PHU->CHIP
+        // possible outcomes:
+        //         FPA     CHIP
+        //         FPA     NONE
+        //         CHIP    NONE
+
+        // mosaicLevel == FPA:
+        // Case    PHU     EXTENSIONS     Modifications
+        // ====    ===     ==========     ===========
+        // 1.      FPA     CHIP           EXT->NONE
+        // 2.      FPA     CELL           EXT->NONE
+        // 3.      FPA     NONE           NONE
+        // 4.      CHIP    CELL           PHU->FPA, EXT->NONE
+        // 5.      CHIP    NONE           PHU->FPA
+        // 6.      CELL    NONE           PHU->FPA
+        // possible outcomes:
+        //         FPA     NONE
+
+        // modify the values of phuItem and extensionsItem
+        switch (mosaicLevel) {
+          case PM_FPA_LEVEL_CHIP:
+            if (!strcasecmp(phuItem->data.str, "FPA") && !strcasecmp(extensionsItem->data.str, "CHIP")) {
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "FPA") && !strcasecmp(extensionsItem->data.str, "CELL")) {
+                psFree(extensionsItem->data.str);
+                extensionsItem->data.str = psStringCopy("CHIP");
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "FPA") && !strcasecmp(extensionsItem->data.str, "NONE")) {
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "CHIP") && !strcasecmp(extensionsItem->data.str, "CELL")) {
+                psFree(extensionsItem->data.str);
+                extensionsItem->data.str = psStringCopy("NONE");
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "CHIP") && !strcasecmp(extensionsItem->data.str, "NONE")) {
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "CELL") && !strcasecmp(extensionsItem->data.str, "NONE")) {
+                psFree(phuItem->data.str);
+                phuItem->data.str = psStringCopy("CHIP");
+                break;
+            }
+            psAbort ("should not reach here");
+
+          case PM_FPA_LEVEL_FPA:
+            if (!strcasecmp(phuItem->data.str, "FPA") && !strcasecmp(extensionsItem->data.str, "CHIP")) {
+                psFree(extensionsItem->data.str);
+                extensionsItem->data.str = psStringCopy("NONE");
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "FPA") && !strcasecmp(extensionsItem->data.str, "CELL")) {
+                psFree(extensionsItem->data.str);
+                extensionsItem->data.str = psStringCopy("NONE");
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "FPA") && !strcasecmp(extensionsItem->data.str, "NONE")) {
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "CHIP") && !strcasecmp(extensionsItem->data.str, "CELL")) {
+                psFree(phuItem->data.str);
+                phuItem->data.str = psStringCopy("FPA");
+                psFree(extensionsItem->data.str);
+                extensionsItem->data.str = psStringCopy("NONE");
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "CHIP") && !strcasecmp(extensionsItem->data.str, "NONE")) {
+                psFree(phuItem->data.str);
+                phuItem->data.str = psStringCopy("FPA");
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "CELL") && !strcasecmp(extensionsItem->data.str, "NONE")) {
+                psFree(phuItem->data.str);
+                phuItem->data.str = psStringCopy("FPA");
+                break;
+            }
+            psAbort ("should not reach here");
+
+          default:
+            psAbort("Should never get here.\n");
+        }
+
+#if 0
+        // XXXX when do I need to adjust the value of CONTENT, CELL.NAME, CHIP.NAME?
+
+        // Don't need a "CONTENT" to identify the content!
+        if (psMetadataLookup(file, "CONTENT")) {
+            psMetadataRemoveKey(file, "CONTENT");
+        }
+        psMetadataAddStr(file, PS_LIST_TAIL, "CONTENT", PS_META_REPLACE, "Key to CONTENTS menu",
+                         "PS_CNTNT");
+        // Don't need CELL.NAME for chip-mosaicked camera
+        if (psMetadataLookup(file, "CELL.NAME")) {
+            psMetadataRemoveKey(file, "CELL.NAME");
+        }
+        // Don't need CHIP.NAME or CELL.NAME for fpa-mosaicked camera
+        if (psMetadataLookup(file, "CHIP.NAME")) {
+            psMetadataRemoveKey(file, "CHIP.NAME");
+        }
+        if (psMetadataLookup(file, "CELL.NAME")) {
+            psMetadataRemoveKey(file, "CELL.NAME");
+        }
+#endif
+
+        // Fix up the CONTENTS to contain only the mosaicked cell for each chip
+        switch (mosaicLevel) {
+          case PM_FPA_LEVEL_FPA:
+            psMetadataAddStr(format, PS_LIST_TAIL, TABLE_OF_CONTENTS, PS_META_REPLACE, NULL,
+                             "MosaickedChip:MosaickedCell:_mosaic");
+            break;
+          case PM_FPA_LEVEL_CHIP:
+            if (!strcasecmp(phuItem->data.str, "FPA") && !strcasecmp(extensionsItem->data.str, "CHIP")) {
+                // ensure the value of CONTENT in the FILE section has the right value
+                psMetadataAddStr(file, PS_LIST_TAIL, "CONTENT", PS_META_REPLACE, "Key to CONTENTS menu",
+                                 "PS_CNTNT");
+                psMetadataAddStr(file, PS_LIST_TAIL, "CONTENT.RULE", PS_META_REPLACE,
+                                 "Rule to generate CONTENTS", "{CHIP.NAME}");
+
+                // List the chipName:chipType for each chip.
+                psMetadata *contents = psMetadataAlloc(); // List of contents, with chipName:chipType
+
+                // XXX this is using the fpaItem->name not the chipName
+                psMetadataIterator *fpaIter = psMetadataIteratorAlloc(fpa, PS_LIST_HEAD, NULL); // Iteratr
+                psMetadataItem *fpaItem;    // Item from iteration
+                while ((fpaItem = psMetadataGetAndIncrement(fpaIter))) {
+                    assert (fpaItem->type == PS_DATA_STRING);
+                    psString content = NULL; // Content to add
+                    psStringAppend(&content, "%s:_mosaicChip ", fpaItem->name);
+                    psMetadataAddStr(contents, PS_LIST_TAIL, fpaItem->name, 0, NULL, content);
+                    psFree(content);
+                }
+                psFree(fpaIter);
+                psMetadataAddMetadata(format, PS_LIST_TAIL, TABLE_OF_CONTENTS, PS_META_REPLACE,
+                                      "List of contents", contents);
+                psFree(contents);
+
+                psMetadata *chips = psMetadataAlloc(); // List of chip types, with cellName:cellType
+                psMetadataAddStr(chips, PS_LIST_TAIL, "_mosaicChip", 0, NULL,
+                                 "MosaickedCell:_mosaic");
+                psMetadataAddMetadata(format, PS_LIST_TAIL, CHIP_TYPES, PS_META_REPLACE,
+                                      "List of chip types", chips);
+                psFree(chips);
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "FPA") && !strcasecmp(extensionsItem->data.str, "NONE")) {
+                // List the contents on a single line
+                psString contentsLine = NULL; // Contents of the PHU
+                psMetadataIterator *fpaIter = psMetadataIteratorAlloc(fpa, PS_LIST_HEAD, NULL); // Iteratr
+                psMetadataItem *fpaItem;    // Item from iteration
+                while ((fpaItem = psMetadataGetAndIncrement(fpaIter))) {
+                    assert (fpaItem->type == PS_DATA_STRING);
+                    psStringAppend(&contentsLine, "%s:MosaickedCell:_mosaic ", fpaItem->name);
+                }
+                psFree(fpaIter);
+                psMetadataAddStr(format, PS_LIST_TAIL, TABLE_OF_CONTENTS, PS_META_REPLACE,
+                                 NULL, contentsLine);
+                psFree(contentsLine);
+                break;
+            }
+            if (!strcasecmp(phuItem->data.str, "CHIP") && !strcasecmp(extensionsItem->data.str, "NONE")) {
+                // XXX recode this to match the structure above (FPA/CHIP)?
+                // select and remove the old contents
+                psMetadata *contents = psMetadataLookupMetadata(NULL, format, TABLE_OF_CONTENTS); // File contents
+                if (!contents) {
+                    psError(PS_ERR_UNKNOWN, false, "Couldn't find %s in the camera format %s.\n",
+                            TABLE_OF_CONTENTS, formatsItem->name);
+                    return false;
+                }
+
+                // replace chip type with _mosaicChip
+                psMetadataIterator *contentIter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL); // Iterator
+                psMetadataItem *contentItem;    // Item from iteration
+                while ((contentItem = psMetadataGetAndIncrement(contentIter))) {
+                    assert (contentItem->type == PS_DATA_STRING);
+                    char *ptr = strchr (contentItem->data.str, ':');
+                    assert (ptr);
+                    psString content = psStringNCopy (contentItem->data.str, ptr - contentItem->data.str);
+                    psStringAppend(&content, ":_mosaicChip ");
+                    psFree (contentItem->data.str);
+                    contentItem->data.str = content;
+                }
+                psFree(contentIter);
+
+                # if (0)
+                while (psListLength(contents->list) > 0) {
+                    psMetadataRemoveIndex(contents, PS_LIST_TAIL);
+                }
+
+                // update with the new contents
+                psMetadataIterator *fpaIter = psMetadataIteratorAlloc(fpa, PS_LIST_HEAD, NULL); // Iterator
+                psMetadataItem *fpaItem;    // Item from iteration
+                while ((fpaItem = psMetadataGetAndIncrement(fpaIter))) {
+                    assert (fpaItem->type == PS_DATA_STRING);
+                    psString content = NULL; // Content to add
+                    psStringAppend(&content, "%s:_mosaicChip ", fpaItem->name);
+                    psMetadataAddStr(contents, PS_LIST_TAIL, fpaItem->name, 0, NULL, content);
+                    psFree(content);
+                }
+                psFree(fpaIter);
+                # endif
+
+                psMetadata *chips = psMetadataAlloc(); // List of chip types, with cellName:cellType
+                psMetadataAddStr(chips, PS_LIST_TAIL, "_mosaicChip", 0, NULL,
+                                 "MosaickedCell:_mosaic");
+                psMetadataAddMetadata(format, PS_LIST_TAIL, CHIP_TYPES, PS_META_REPLACE,
+                                      "List of chip types", chips);
+                psFree(chips);
+                break;
+            }
+        default:
+            psAbort("Should never get here.\n");
+        }
+
+        // Fix the cell type
+        psMetadata *cells = psMetadataLookupMetadata(NULL, format, CELL_TYPES); // CELLS information
+        if (!cells) {
+            psError(PS_ERR_UNKNOWN, false, "Couldn't find CELLS of type METADATA in the camera format %s.\n", formatsItem->name);
+            return false;
+        }
+        psMetadata *cell = psMetadataAlloc(); // Cell information
+        psMetadataAddStr(cell, PS_LIST_TAIL, "CELL.TRIMSEC", 0, "Trim section", "TRIMSEC");
+        psMetadataAddStr(cell, PS_LIST_TAIL, "CELL.BIASSEC", 0, "Bias section", "BIASSEC");
+        psMetadataAddStr(cell, PS_LIST_TAIL, "CELL.TRIMSEC.SOURCE", 0, "Trim section source", "HEADER");
+        psMetadataAddStr(cell, PS_LIST_TAIL, "CELL.BIASSEC.SOURCE", 0, "Bias section source", "HEADER");
+        psMetadataAddMetadata(cells, PS_LIST_HEAD, "_mosaic", PS_META_REPLACE, "Mosaic cell information", cell);
+        psFree(cell);                   // Drop reference
+
+        // Update the concepts, so that they are all stored in the FITS headers, under headers of the same
+        // name as the concept
+        psMetadata *database = psMetadataLookupMetadata(&mdok, format, "DATABASE"); // DATABASE concepts
+        psMetadata *defaults = psMetadataLookupMetadata(&mdok, format, "DEFAULTS"); // DEFAULTS concepts
+        if (!mdok || !defaults) {
+            psWarning("Couldn't find DEFAULTS of type METADATA in the camera format %s.\n",
+                      formatsItem->name);
+            continue;
+        }
+        psMetadata *translation = psMetadataLookupMetadata(&mdok, format, "TRANSLATION"); // TRANSLATION info
+        if (!mdok || !translation) {
+            psWarning("Couldn't find TRANSLATION of type METADATA in the camera format %s.\n",
+                      formatsItem->name);
+            continue;
+        }
+
+        removeCellConceptsSources(translation);
+        removeCellConceptsSources(database);
+        removeCellConceptsSources(defaults);
+
+        psMetadata *conceptFormats = psMetadataLookupMetadata(&mdok, format, "FORMATS"); // Concepts formats
+
+        // Add in the positioning concepts
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.XPARITY", 0, NULL, 1);
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.YPARITY", 0, NULL, 1);
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.X0",      0, NULL, 0);
+        psMetadataAddS32(defaults, PS_LIST_TAIL, "CELL.Y0",      0, NULL, 0);
+        if (conceptFormats) {
+            if (psMetadataLookup(conceptFormats, "CELL.X0")) {
+                psMetadataRemoveKey(conceptFormats, "CELL.X0");
+            }
+            if (psMetadataLookup(conceptFormats, "CELL.Y0")) {
+                psMetadataRemoveKey(conceptFormats, "CELL.Y0");
+            }
+        }
+
+        if (mosaicLevel == PM_FPA_LEVEL_FPA) {
+            removeChipConceptsSources(translation);
+            removeChipConceptsSources(database);
+            removeChipConceptsSources(defaults);
+            psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.XPARITY", 0, NULL, 1);
+            psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.YPARITY", 0, NULL, 1);
+            psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.X0", 0, NULL, 0);
+            psMetadataAddS32(defaults, PS_LIST_TAIL, "CHIP.Y0", 0, NULL, 0);
+            if (conceptFormats) {
+                if (psMetadataLookup(conceptFormats, "CHIP.X0")) {
+                    psMetadataRemoveKey(conceptFormats, "CHIP.X0");
+                }
+                if (psMetadataLookup(conceptFormats, "CHIP.Y0")) {
+                    psMetadataRemoveKey(conceptFormats, "CHIP.Y0");
+                }
+            }
+        }
+
+    }
+    psFree(formatsIter);
+
+    // New camera MUST go to the head of the metadata, so that it will be recognised first
+    // The old camera doesn't contain the PSMOSAIC header, so it will match anything!
+    psMetadataAddMetadata(newCameras, PS_LIST_HEAD, newName, PS_META_REPLACE,
+                          "Automatically generated", new);
+    psFree(newName);
+    psFree(new);
+
+    return true;
+}
+
+/*** Helper Functions ***/
+
+// Remove a concept from the list of sources.  Need to check to see if it exists first, to avoid a warning.
+static void removeConcept(psMetadata *source, // Source from which to remove concept
+                          const char *concept // Concept name to remove
+                         )
+{
+    assert(source);
+    assert(concept && strlen(concept) > 0);
+
+    if (psMetadataLookup(source, concept)) {
+        psMetadataRemoveKey(source, concept);
+    }
+
+    return;
+}
+
+// Remove certain concepts from the list of sources.  These concepts are important in the mosaicking process,
+// and are added explicitly to the defaults (elsewhere) so that the user can't get them wrong.
+static void removeCellConceptsSources(psMetadata *source // Source for concepts
+    )
+{
+    if (!source) {
+        return;
+    }
+
+    removeConcept(source, "CELL.BIASSEC");
+    removeConcept(source, "CELL.TRIMSEC");
+    removeConcept(source, "CELL.XPARITY");
+    removeConcept(source, "CELL.YPARITY");
+    removeConcept(source, "CELL.X0");
+    removeConcept(source, "CELL.Y0");
+
+    // For the sake of the defaults, include the .DEPEND
+    removeConcept(source, "CELL.XPARITY.DEPEND");
+    removeConcept(source, "CELL.YPARITY.DEPEND");
+    removeConcept(source, "CELL.X0.DEPEND");
+    removeConcept(source, "CELL.Y0.DEPEND");
+
+    return;
+}
+
+// Remove certain concepts from the list of sources.  These concepts are important in the mosaicking process,
+// and are added explicitly to the defaults (elsewhere) so that the user can't get them wrong.
+static void removeChipConceptsSources(psMetadata *source // Source for concepts
+    )
+{
+    if (!source) {
+        return;
+    }
+
+    removeConcept(source, "CHIP.XPARITY");
+    removeConcept(source, "CHIP.YPARITY");
+    removeConcept(source, "CHIP.X0");
+    removeConcept(source, "CHIP.Y0");
+
+    // For the sake of the defaults, include the .DEPEND
+    removeConcept(source, "CHIP.XPARITY.DEPEND");
+    removeConcept(source, "CHIP.YPARITY.DEPEND");
+    removeConcept(source, "CHIP.X0.DEPEND");
+    removeConcept(source, "CHIP.Y0.DEPEND");
+
+    return;
+}
Index: /branches/eam_branch_20071023/psModules/src/config/pmConfigCamera.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmConfigCamera.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmConfigCamera.h	(revision 15358)
@@ -0,0 +1,52 @@
+/*  @file pmConfigCamera.h
+ *  @brief Camera Configuration functions
+ *
+ *  @author Paul Price, IfA
+ *  @author Eugene Magnier, IfA
+ *
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-08-14 00:17:40 $
+ *  Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONFIG_CAMERA_H
+#define PM_CONFIG_CAMERA_H
+
+/// @addtogroup Config Configuration System
+/// @{
+
+// Generate a skycell version of a camera configuration
+bool pmConfigGenerateSkycellVersion(psMetadata *oldCameras, // Old list of camera configurations
+                                    psMetadata *newCameras, // New list of camera configurations
+                                    const char *name, // Name of original camera configuration
+                                    const psMetadata *site // The site configuration
+    );
+
+/// Generate the skycell version of a particular camera configuration
+bool pmConfigCameraSkycellVersion(psMetadata *site, // The site configuration
+                                  const char *name // Name of the un-mosaicked camera
+    );
+
+
+/// Generate skycell versions of all the camera configurations
+bool pmConfigCameraSkycellVersionsAll(psMetadata *site // Site configuration
+    );
+
+// Generate a mosaicked version of a camera configuration
+bool pmConfigGenerateMosaickedVersion(psMetadata *oldCameras, // Old list of camera configurations
+                                      psMetadata *newCameras, // New list of camera configurations
+                                      const char *name, // Name of original camera configuration
+                                      pmFPALevel mosaicLevel // Level to which we are mosaicking
+    );
+
+/// Generate the chip mosaicked version of a particular camera configuration
+bool pmConfigCameraMosaickedVersions(psMetadata *site, // Site configuration
+                                     const char *name // Name of the un-mosaicked camera
+    );
+
+/// Generate chip- and fpa-mosaicked versions of all the camera configurations
+bool pmConfigCameraMosaickedVersionsAll(psMetadata *site // Site configuration
+    );
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/config/pmConfigCommand.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmConfigCommand.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmConfigCommand.c	(revision 15358)
@@ -0,0 +1,59 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmConfig.h"
+#include "pmConfigCommand.h"
+
+bool pmConfigDatabaseCommand(psString *command, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(command, false);
+
+    bool mdok;                          // Status of MD lookup
+    const char *dbserver = psMetadataLookupStr(&mdok, config->site, "DBSERVER"); // Database server
+    if (!mdok || strlen(dbserver) == 0) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find DBSERVER in site configuration.\n");
+        return NULL;
+    }
+    const char *dbname = psMetadataLookupStr(&mdok, config->site, "DBNAME"); // Database name
+    if (!mdok || strlen(dbname) == 0) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find DBNAME in site configuration.\n");
+        return NULL;
+    }
+    const char *dbuser = psMetadataLookupStr(&mdok, config->site, "DBUSER"); // Database user
+    if (!mdok || strlen(dbuser) == 0) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find DBUSER in site configuration.\n");
+        return NULL;
+    }
+    const char *dbpassword = psMetadataLookupStr(&mdok, config->site, "DBPASSWORD"); // Database password
+    if (!mdok || strlen(dbpassword) == 0) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find DBPASSWORD in site configuration.\n");
+        return NULL;
+    }
+
+    psStringAppend(command, " -dbserver %s -dbname %s -dbuser %s -dbpassword %s",
+                   dbserver, dbname, dbuser, dbpassword);
+
+    return true;
+}
+
+
+bool pmConfigTraceCommand(psString *command)
+{
+    PS_ASSERT_PTR_NON_NULL(command, false);
+
+    psMetadata *levels = psTraceLevels(); // Metadata levels
+    psMetadataIterator *iter = psMetadataIteratorAlloc(levels, PS_LIST_HEAD, NULL); // Iterator for levels
+    psMetadataItem *item;               // Item from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        assert(item->type == PS_TYPE_S32);
+        psStringAppend(command, " -trace %s %d", item->name, item->data.S32);
+    }
+    psFree(iter);
+    psFree(levels);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/config/pmConfigCommand.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmConfigCommand.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmConfigCommand.h	(revision 15358)
@@ -0,0 +1,16 @@
+#ifndef PM_CONFIG_COMMAND_H
+#define PM_CONFIG_COMMAND_H
+
+/// Extend a command-line to include the necessary database flags
+///
+/// The command-line is extended with -dbserver, -dbname, -dbuser, -dbpassword
+bool pmConfigDatabaseCommand(psString *command, ///< Command to extend
+                             const pmConfig *config ///< Configuration
+                            );
+
+/// Extend a command-line to propagate the trace flags
+///
+/// The command-line is extended with -trace
+bool pmConfigTraceCommand(psString *command ///< Command to extend
+                         );
+#endif
Index: /branches/eam_branch_20071023/psModules/src/config/pmConfigMask.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmConfigMask.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmConfigMask.c	(revision 15358)
@@ -0,0 +1,40 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfigMask.h"
+
+
+psMaskType pmConfigMask(const char *masks, const pmConfig *config)
+{
+    PS_ASSERT_STRING_NON_EMPTY(masks, 0);
+
+    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, "MASKS"); // The recipe
+    if (!recipe) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
+        return 0;
+    }
+
+    psMaskType mask = 0;                // Mask value, to return
+
+    psArray *names = psStringSplitArray(masks, " ,;", false); // Array of symbolic names
+    for (int i = 0; i < names->n; i++) {
+        const char *name = names->data[i]; // Symbolic name of interest
+        bool mdok;                      // Status of MD lookup
+        psMaskType value = psMetadataLookupU8(&mdok, recipe, name);
+        if (!mdok) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, false,
+                    "Unable to find mask value for %s in MASK recipe", name);
+            psFree(names);
+            return 0;
+        }
+        mask |= value;
+    }
+    psFree(names);
+
+    return mask;
+}
Index: /branches/eam_branch_20071023/psModules/src/config/pmConfigMask.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmConfigMask.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmConfigMask.h	(revision 15358)
@@ -0,0 +1,30 @@
+/*  @file pmConfigMask.h
+ *  @brief Mask configuration functions
+ *
+ *  @author Paul Price, IfA
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-06-02 03:51:03 $
+ *  Copyright 2007 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONFIG_MASK_H
+#define PM_CONFIG_MASK_H
+
+#include <pslib.h>
+#include <pmConfig.h>
+
+#define PM_MASKS_RECIPE "MASKS"
+
+/// @addtogroup Config Configuration System
+/// @{
+
+/// Return a mask value given a list of symbolic names
+///
+/// The mask values are derived from the MASKS recipe
+psMaskType pmConfigMask(const char *masks, ///< List of symbolic names, space/comma delimited
+                        const pmConfig *config ///< Configuration
+                        );
+
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/config/pmConfigRecipes.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmConfigRecipes.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmConfigRecipes.c	(revision 15358)
@@ -0,0 +1,548 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>		/* for strn?casecmp */
+#include <unistd.h>
+#include <libgen.h>
+#include <assert.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <glob.h>
+#include <pslib.h>
+#include "pmConfig.h"
+#include "pmConfigRecipes.h"
+
+static bool loadRecipeSite(bool *status, pmConfig *config, psMetadata *source);
+static bool loadRecipeCamera(bool *status, pmConfig *config, psMetadata *source);
+static bool loadRecipeFromArguments(bool *status, pmConfig *config);
+static bool loadRecipeSymbols(bool *status, pmConfig *config);
+static bool loadRecipeOptions(bool *status, pmConfig *config);
+
+// use this function to select the options structure for the specified recipe
+// add additional command-line options to this metadata (before parsing the camera)
+psMetadata *pmConfigRecipeOptions (pmConfig *config, char *recipeName)
+{
+    bool success;
+
+    // select or create the OPTIONS folder
+    psMetadata *options = psMetadataLookupMetadata(&success, config->arguments, "OPTIONS");
+    if (!options) {
+        options = psMetadataAlloc ();
+        success = psMetadataAddPtr (config->arguments, PS_LIST_TAIL, "OPTIONS",  PS_DATA_METADATA, "", options);
+	assert (success); // type mismatch : OPTIONS already defined but wrong type
+	psFree (options); // drop extra reference
+    }
+
+    // look for the recipe defined in recipes
+    // if the recipe is already defined in config->arguments:OPTIONS, supplement
+    // save the recipe options onto config->arguments:RECIPES
+    psMetadata *recipe = psMetadataLookupMetadata(&success, options, recipeName);
+    if (!recipe) {
+        recipe = psMetadataAlloc();
+        success = psMetadataAddPtr(options, PS_LIST_TAIL, recipeName,  PS_DATA_METADATA, "", recipe);
+	assert (success); // type mismatch : OPTIONS already defined but wrong type
+	psFree (recipe);  // drop extra reference
+    }
+    return recipe;
+}
+
+// this function may be called several times.  it attempts to load the recipe data from one of
+// three locations: config->site, config->camera, and argv.  We cannot read the recipes
+// from config->camera until a camera has been read BUT, the argv recipes must override the
+// camera and site recipes.
+bool pmConfigReadRecipes(pmConfig *config, pmRecipeSource source)
+{
+    bool status;
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    if (!config->recipes) {
+        config->recipes = psMetadataAlloc();
+    }
+
+    // Read the recipe file names from the site configuration and camera configuration
+    // It is an error for config->site:recipes not to exist.  all programs install their
+    // master recipe files in the site:recipe location when they are built.
+    if (config->site && (source & PM_RECIPE_SOURCE_SITE)) {
+	if (!loadRecipeSite(&status, config, config->site)) {
+            psError(PS_ERR_IO, false, "Failed to read recipes from site config");
+	    return false;
+        } 
+	psTrace ("psModules.config", 3, "read recipes from site config");
+    }
+
+    // camera-specific recipes are not required : note the absence with a message
+    // camera-specific recipes may be read for a specified camera (in pmConfigRead) or
+    // for an identified camera (in pmConfigCameraFormatFromHeader).  the second
+    // set should not override the first set
+    if (config->camera && (source & PM_RECIPE_SOURCE_CAMERA) && !(config->recipesRead & PM_RECIPE_SOURCE_CAMERA)) {
+        if (!loadRecipeCamera(&status, config, config->camera)) {
+            psError(PS_ERR_IO, false, "Failed to read recipes from camera config");
+	    return false;
+        } 
+	if (status) {
+	    psTrace ("psModules.config", 3, "read recipes from camera config");
+	} else {
+	    psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied by camera config");
+	}
+    }
+
+    // apply recipes loaded into config->arguments based on command-line arguments
+    if (config->arguments && (source & PM_RECIPE_SOURCE_CL)) {
+        if (!loadRecipeFromArguments(&status, config)) {
+            psError(PS_ERR_IO, false, "Failed to read recipes from command-line arguments");
+	    return false;
+        } 
+	if (status) {
+	    psTrace ("psModules.config", 3, "read recipes from command-line arguments");
+	} else {
+	    psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied on command-line arguments");
+	}
+        if (!loadRecipeSymbols(&status, config)) {
+            psError(PS_ERR_IO, false, "Failed to read recipes from symbolic references");
+	    return false;
+        } 
+	if (status) {
+	    psTrace ("psModules.config", 3, "read recipes from symbolic references");
+	} else {
+	    psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied by symbolic reference");
+	}
+        if (!loadRecipeOptions(&status, config)) {
+            psError(PS_ERR_IO, false, "Failed to read recipes from symbolic references");
+	    return false;
+        } 
+	if (status) {
+            psTrace ("psModules.config", 3, "read recipes from command-line arguments");
+        } else {
+            psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied on command-line arguments");
+        }
+    }
+    return true;
+}
+
+// search for options of the form -D KEY VALUE or -D RECIPE:KEY VALUE
+bool pmConfigLoadRecipeOptions (int *argc, char **argv, pmConfig *config, char *flag)
+{
+    bool success;
+    int argNum;
+
+    // save the recipes onto config->arguments:RECIPES
+    // increment so we can free below (is a NOP if 'options' is NULL)
+    psMetadata *options = psMetadataLookupMetadata(&success, config->arguments, "OPTIONS");
+    if (!options) {
+        options = psMetadataAlloc();
+        success = psMetadataAddPtr(config->arguments, PS_LIST_TAIL, "OPTIONS",  PS_DATA_METADATA, "Command-line options specified with -D", options);
+	assert (success); // type mismatch : OPTIONS already defined but wrong type
+	psFree (options); // drop extra reference
+    }
+
+    // -D key value (all added as string)
+    while ((argNum = psArgumentGet (*argc, argv, flag))) {
+        psArgumentRemove (argNum, argc, argv);
+
+        // do we have enough arguments?
+        if (argNum + 1 >= *argc) {
+            psError(PS_ERR_IO, true, "insufficient parameters for command-line argument -D");
+            return false;
+        }
+
+        // is a target recipe specified?
+        const char *recipeName = NULL;
+        char *key;
+        psArray *words = psStringSplitArray(argv[argNum], ":", false);
+        switch (words->n) {
+        case 1:
+            recipeName = config->defaultRecipe;
+            if (!config->defaultRecipe) {
+                psError(PS_ERR_IO, true, "syntax error in parameter: no default recipe available; must specify recipe");
+                return false;
+            }
+            key = words->data[0];
+            break;
+        case 2:
+            recipeName = words->data[0];
+            key = words->data[1];
+            break;
+        default:
+            psError(PS_ERR_IO, true, "syntax error in parameter");
+            return false;
+        }
+
+        // if this recipe is already defined in options, supplement
+        psMetadata *recipe = psMetadataLookupMetadata(&success, options, recipeName);
+        if (!recipe) {
+            recipe = psMetadataAlloc();
+            success = psMetadataAddPtr(options, PS_LIST_TAIL, recipeName,  PS_DATA_METADATA, "", recipe);
+	    assert (success); // type mismatch : recipe already defined but wrong type
+	    psFree (recipe); // drop extra reference
+        }
+
+        bool valid = false;
+        if (!strcmp (flag, "-D")) {
+            psMetadataAddStr (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", argv[argNum+1]);
+            valid = true;
+        }
+        if (!strcmp (flag, "-Di")) {
+            psMetadataAddS32 (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", atoi(argv[argNum+1]));
+            valid = true;
+        }
+        if (!strcmp (flag, "-Df")) {
+            psMetadataAddF32 (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", atof(argv[argNum+1]));
+            valid = true;
+        }
+        if (!strcmp (flag, "-Db")) {
+            if (!strcasecmp (argv[argNum+1], "true")) {
+                psMetadataAddBool (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", true);
+            } else {
+                psMetadataAddBool (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", false);
+            }
+            valid = true;
+        }
+        psFree (words);
+        assert (valid);  // flag may be: -D, -Df, -Di, -Db
+
+        psArgumentRemove (argNum, argc, argv);
+        psArgumentRemove (argNum, argc, argv);
+    }
+    return true;
+}
+
+// examine command-line arguments for -recipe KEY VALUE
+// if VALUE is an existing file, read as metadata and save on config->arguments with name = KEY
+// if VALUE is not an exiting file, it is a symbolic lookup: it is saved on config->recipeSymbols
+// for later interpolation (pmConfigReadRecipes with option CL)
+bool pmConfigLoadRecipeArguments (int *argc, char **argv, pmConfig *config)
+{
+    bool success; 
+
+    psMetadata *recipes = psMetadataLookupMetadata(&success, config->arguments, "RECIPES");
+    if (!recipes) {
+        recipes = psMetadataAlloc();
+        success = psMetadataAddPtr (config->arguments, PS_LIST_TAIL, "RECIPES",  PS_DATA_METADATA, "", recipes);
+	assert (success);
+	psFree (recipes);
+    }
+
+    // Go through the command-line arguments
+    int argNum; // Argument number for "-recipe"
+    while ((argNum = psArgumentGet(*argc, argv, "-recipe")) > 0) {
+        psArgumentRemove(argNum, argc, argv);
+        if (argNum + 1 >= *argc) {
+            psError(PS_ERR_IO, false, "-recipe command-line switch provided without the required recipe and source\n");
+            return false;
+        }
+
+        char *recipeName = psStringCopy(argv[argNum]); // Name of the recipe
+        psArgumentRemove(argNum, argc, argv);
+        char *recipeSource = psStringCopy(argv[argNum]); // Source of the recipe
+        psArgumentRemove(argNum, argc, argv);
+
+        if (access(recipeSource, R_OK) == 0) {
+            // The source is a file: load onto config->arguments
+            // save the recipes onto config->arguments:RECIPES
+
+            psMetadata *recipe = NULL;      // Recipe from file
+            if (pmConfigFileRead(&recipe, recipeSource, "recipe")) {
+                psString comment = psStringCopy("Recipe added at command line from ");
+                psStringAppend(&comment, "%s", recipeSource);
+                psMetadataAdd(recipes, PS_LIST_TAIL, recipeName, PS_DATA_METADATA | PS_META_REPLACE,
+                              comment, recipe);
+                psFree(comment);
+                psFree(recipe);                 // Drop reference
+            } else {
+                psAbort("error reading config file %s\n", recipeSource);
+            }
+        } else {
+            // Assume it's a symbolic reference to something that's not yet read in.
+            // it will be loaded later by pmConfigReadRecipes with option CL
+            psMetadataAddStr(config->recipeSymbols, PS_LIST_TAIL, recipeName, PS_META_REPLACE, "",
+                             recipeSource);
+        }
+        psTrace ("psModules.config", 3, "read recipe %s from %s", recipeName, recipeSource);
+        psFree(recipeName);
+        psFree(recipeSource);
+    } // Iterating through the command-line arguments
+
+    return true;
+}
+
+// Load the recipe files for SITE : REQUIRED
+static bool loadRecipeSite(bool *status,
+			   pmConfig *config, // The configuration into which to read the recipes
+			   psMetadata *source // The source configuration, from which to read the filenames
+    )
+{
+    assert(status);
+    assert(config);
+    *status = false;
+
+    if (!source) {
+        psError(PS_ERR_IO, true, "The site configuration has not been read --- cannot read recipes from this location.\n");
+        config->recipesRead &= ~PM_RECIPE_SOURCE_SITE;
+        return false;
+    }
+
+    psMetadata *recipes = psMetadataLookupMetadata(NULL, source, "RECIPES"); // The list of recipes
+    if (!recipes) {
+        psError(PS_ERR_IO, false, "RECIPES not found in the site configuration\n");
+        return false;
+    }
+
+    // Copy contents of the filenames to config->recipes from the "RECIPES" metadata in the source.
+    // We could use psMetadataCopy for this, but it's better to check that everything's of the correct type.
+    // If it's not of the correct type, we can tell the user which file it's in, so they can find it easier.
+    psMetadataIterator *recipesIter = psMetadataIteratorAlloc(recipes, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *fileItem = NULL;    // MD item containing the filename, from recipe iteration
+    while ((fileItem = psMetadataGetAndIncrement(recipesIter))) {
+        // type mismatch is a serious error
+        if (fileItem->type != PS_DATA_STRING) {
+            psError(PS_ERR_IO, true, "%s in site configuration RECIPES is not of type STR", fileItem->name);
+	    return false;
+        }
+
+        // Read the recipe file. if we fail on a file, give a warning, but continue
+        psMetadata *recipe = NULL;
+        if (!pmConfigFileRead(&recipe, fileItem->data.V, "recipe")) {
+            psError(PS_ERR_IO, false, "Failed to read recipe file %s listed in site configuration\n", (char *)fileItem->data.V);
+	    return false;
+        }
+
+        // if this named recipe exists, supplement it
+	psMetadataAdd(config->recipes, PS_LIST_TAIL, fileItem->name, PS_DATA_METADATA | PS_META_REPLACE,
+		      fileItem->comment, recipe);
+        psFree(recipe);  // Drop reference
+    }
+    psFree(recipesIter);
+    config->recipesRead |= PM_RECIPE_SOURCE_SITE;
+
+    *status = true;
+    return true;
+}
+
+// Load the recipe files (valid for SITE | CAMERA)
+// each time we load a specific recipe, it overrides the existing metadata for that recipe
+// for sourceType == SITE | CAMERA, RECIPES contains a list of files to be read (pmConfigFileRead)
+static bool loadRecipeCamera(bool *status, // status variable
+			     pmConfig *config, // The configuration into which to read the recipes
+			     psMetadata *source // The source configuration, from which to read the filenames
+    )
+{
+    bool success;
+
+    assert(status);
+    assert(config);
+    *status = false;
+
+    if (!source) {
+        psError(PS_ERR_IO, true, "The camera configuration has not been read --- cannot read recipes from this location.\n");
+        config->recipesRead &= ~PM_RECIPE_SOURCE_CAMERA;
+        return false;
+    }
+
+    // it is not necessary to define any local recipes in the camera config
+    psMetadata *recipes = psMetadataLookupMetadata(&success, source, "RECIPES"); // The list of recipes
+    if (!recipes) {
+	psTrace ("psModules.config", 3, "RECIPES not found in the camera configuration\n");
+        return true;
+    }
+
+    // Copy contents of the filenames to config->recipes from the "RECIPES" metadata in the source.
+    // We could use psMetadataCopy for this, but it's better to check that everything's of the correct type.
+    // If it's not of the correct type, we can tell the user which file it's in, so they can find it easier.
+    psMetadataIterator *recipesIter = psMetadataIteratorAlloc(recipes, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *fileItem = NULL;    // MD item containing the filename, from recipe iteration
+    while ((fileItem = psMetadataGetAndIncrement(recipesIter))) {
+	char *recipeName = fileItem->name;
+	char *recipeFile = fileItem->data.V;
+
+	psTrace("psModules.config", 3, "Supplementing %s from %s within camera configuration.\n", recipeName, recipeFile);
+
+        // type mismatch is a serious error
+        if (fileItem->type != PS_DATA_STRING) {
+            psAbort("%s in camera configuration RECIPES is not of type STR", recipeName);
+        }
+
+        // Read the recipe file. if we fail on a file, give a warning, but continue
+        psMetadata *recipe = NULL;
+        if (!pmConfigFileRead(&recipe, recipeFile, "recipe")) {
+            psError(PS_ERR_IO, false, "Failed to read recipe file %s listed in camera configuration\n", recipeFile);
+	    return false;
+        }
+
+        // the named recipe must exist; supplement it
+	psMetadata *current = psMetadataLookupMetadata(NULL, config->recipes, recipeName);
+	if (!current) {
+	    psError(PS_ERR_IO, false, "Failed to find recipe for %s in master recipe list", recipeName);
+	    psFree(recipe);  // Drop reference
+	    return false;
+	}
+
+	if (!psMetadataUpdate(current, recipe)) {
+	    psError(PS_ERR_IO, false, "Failed to update recipe for %s from camera recipe", recipeName);
+	    psFree(recipe);  // Drop reference
+	    return false;
+	}
+        psFree(recipe);  // Drop reference
+    }
+    psFree(recipesIter);
+    config->recipesRead |= PM_RECIPE_SOURCE_CAMERA;
+    *status = true;
+    return true;
+}
+
+// Load the recipes from config->arguments (CL)
+// Load the recipes: each time we load a specific recipe, it overrides the metadata
+// entries for an existing recipe metadata
+static bool loadRecipeFromArguments(bool *status,
+				    pmConfig *config // The configuration into which to read the recipes
+    )
+{
+    assert(status);
+    assert(config);
+    *status = false;
+
+    if (!config->arguments) {
+        psTrace("psModules.config", 4, "no config->arguments metadata, nothing to read here");
+        return true;
+    }
+
+    psMetadata *recipes = psMetadataLookupMetadata(NULL, config->arguments, "RECIPES"); // The list of recipes
+    if (!recipes) {
+        psTrace("psModules.config", 4, "no RECIPES in config->arguments, nothing to read here");
+        return true;
+    }
+
+    // Copy the recipes from config->arguments:RECIPES to config->recipes.  supplement existing recipes.
+    psMetadataIterator *recipesIter = psMetadataIteratorAlloc(recipes, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item = NULL;    // MD item containing the filename, from recipe iteration
+    while ((item = psMetadataGetAndIncrement(recipesIter))) {
+        // type mismatch is a serious error
+        if (item->type != PS_DATA_METADATA) {
+            psAbort("%s in config arguments RECIPES is not of type METADATA", item->name);
+        }
+        // increment the ref counter to protect the data
+
+        psMetadata *recipe = item->data.V; // Recipe of interest
+
+        // if this named recipe exists, supplement it
+        psMetadata *current = psMetadataLookupMetadata(NULL, config->recipes, item->name);
+        if (!current) {
+	    psError(PS_ERR_IO, false, "Failed to find recipe for %s in master recipe list", item->name);
+	    psFree(recipe);  // Drop reference
+	    return false;
+	}
+	psTrace("psModules.config", 3, "Supplementing %s from arguments.\n", item->name);
+
+        if (!psMetadataUpdate (current, recipe)) {
+	    psError(PS_ERR_IO, false, "Failed to update recipe for %s from camera recipe", item->name);
+	    return false;
+	}
+    }
+    psFree(recipesIter);
+    *status = true;
+    return true;
+}
+
+// Load the recipes: each time we load a specific recipe, it overrides the metadata
+// entries for an existing recipe metadata
+static bool loadRecipeSymbols(bool *status,
+			      pmConfig *config // The configuration into which to read the recipes
+    )
+{
+    bool found = false;
+
+    assert(status);
+    assert(config);
+    *status = false;
+
+    // check to see if any symbolic names need to be resolved
+    // each entry in recipeSymbols are of the form TARGET=SOURCE where TARGET is an existing
+    // recipe MD and REF is a MD to load over that recipe
+    psMetadataIterator *iter = psMetadataIteratorAlloc(config->recipeSymbols, PS_LIST_HEAD, NULL);
+    psMetadataItem *item = NULL;  // Item containing source, from iteration
+    while ((item = psMetadataGetAndIncrement(iter))) {
+        assert(item->type == PS_DATA_STRING); // It should be this type: we put it in ourselves
+        const char *sourceName = item->data.V; // The name of the symbolic reference
+        const char *targetName = item->name;
+	psTrace("psModules.config", 3, "Supplementing %s from %s.\n", targetName, sourceName);
+
+	// the target recipe must exist; select it
+	psMetadata *targetMD = psMetadataLookupMetadata(&found, config->recipes, targetName);
+	if (!targetMD) {
+	    psError(PS_ERR_IO, true, "Failed to find recipe for %s in master recipe list", targetName);
+	    return false;
+	}
+
+        // search for sourceName : it may be in config->recipes or target MD
+        psMetadata *sourceMD = NULL;
+	sourceMD = psMetadataLookupMetadata(&found, config->recipes, sourceName);
+        if (!sourceMD) {
+	    sourceMD = psMetadataLookupMetadata(&found, targetMD, sourceName);
+	    if (!sourceMD) {
+		psError(PS_ERR_IO, false, "Selected symbolic name %s does not exist in recipes", sourceName);
+		return false;
+	    }
+	}
+
+	if (!psMetadataUpdate(targetMD, sourceMD)) {
+	    psError(PS_ERR_IO, false, "Failed to update recipe for %s from camera recipe", targetName);
+	    return false;
+	}
+    }
+    psFree(iter);
+    *status = true;
+    return true;
+}
+
+// Load the recipe options
+// Load the recipes: each time we load a specific recipe, it overrides the metadata
+// entries for an existing recipe metadata
+static bool loadRecipeOptions(bool *status,
+			      pmConfig *config // The configuration into which to read the recipes
+    )
+{
+    bool found;
+    assert(status);
+    assert(config);
+    *status = false;
+
+    if (!config->arguments) {
+        psTrace("psModules.config", 4, "no config->arguments metadata, nothing to read here");
+        return true;
+    }
+
+    psMetadata *recipes = psMetadataLookupMetadata(&found, config->arguments, "OPTIONS"); // The list of recipes
+    if (!recipes) {
+        psTrace("psModules.config", 4, "no OPTIONS in config->arguments, nothing to read here");
+        return true;
+    }
+
+    // Copy the recipes from config->arguments:OPTIONS to config->recipes.  supplement existing recipes.
+    psMetadataIterator *recipesIter = psMetadataIteratorAlloc(recipes, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item = NULL;    // MD item containing the filename, from recipe iteration
+    while ((item = psMetadataGetAndIncrement(recipesIter))) {
+	char *recipeName = item->name;
+        psMetadata *recipe = item->data.V;
+
+        // type mismatch is a serious error
+        if (item->type != PS_DATA_METADATA) {
+            psAbort("%s in config arguments OPTIONS is not of type METADATA", recipeName);
+        }
+
+        // if this named recipe exists, supplement it
+        psMetadata *current = psMetadataLookupMetadata(NULL, config->recipes, recipeName);
+        if (!current) {
+	    psError(PS_ERR_IO, false, "Selected recipe %s is not found in camera recipe", recipeName);
+	    return false;
+	}
+        if (!psMetadataUpdate (current, recipe)) {
+	    psError(PS_ERR_IO, false, "Failed to update recipe for %s from camera recipe", recipeName);
+	    return false;
+	}
+    }
+    psFree(recipesIter);
+    *status = true;
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/config/pmConfigRecipes.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmConfigRecipes.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmConfigRecipes.h	(revision 15358)
@@ -0,0 +1,33 @@
+/*  @file pmConfigRecipes.h
+ *  @brief Configuration Recipe functions
+ * 
+ *  @author ?, MHPCC
+ *  @author Paul Price, IfA
+ *  @author Eugene Magnier, IfA
+ * 
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-04-19 02:10:12 $
+ *  Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_CONFIG_RECIPES_H
+#define PM_CONFIG_RECIPES_H
+
+/// @addtogroup Config Configuration System
+/// @{
+
+/// Read recipes
+///
+/// Attempt to read recipes from the sources that are available but have not already been read.  Having read a
+/// recipe, attempt to resolve symbolic links that were specified on the command line.
+bool pmConfigReadRecipes(pmConfig *config, ///< Configuration
+                         pmRecipeSource source ///< desired sources for recipes
+                        );
+
+
+bool pmConfigLoadRecipeArguments (int *argc, char **argv, pmConfig *config);
+bool pmConfigLoadRecipeOptions (int *argc, char **argv, pmConfig *config, char *flag);
+psMetadata *pmConfigRecipeOptions (pmConfig *config, char *recipe);
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/config/pmErrorCodes.c.in
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmErrorCodes.c.in	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmErrorCodes.c.in	(revision 15358)
@@ -0,0 +1,25 @@
+/*
+ * The line
+    { PM_ERR_$X{ErrorCode}, "$X{ErrorDescription}"},
+ * (without the Xs)
+ * will be replaced by values from errorCodes.dat
+ */
+#include <pslib.h>
+#include "pmErrorCodes.h"
+
+void pmErrorRegister(void)
+{
+    static psErrorDescription errors[] = {
+       { PM_ERR_BASE, "First value we use; lower values belong to psLib" },
+       { PM_ERR_${ErrorCode}, "${ErrorDescription}"},
+    };
+    static int nerror = PM_ERR_NERROR - PM_ERR_BASE; // number of values in enum
+
+    for (int i = 0; i < nerror; i++) {
+       psErrorDescription *tmp = psAlloc(sizeof(psErrorDescription));
+       *tmp = errors[i];
+       psErrorRegister(tmp, 1);
+       psFree(tmp);			/* it's on the internal list */
+    }
+    nerror = 0;			                // don't register more than once
+}
Index: /branches/eam_branch_20071023/psModules/src/config/pmErrorCodes.dat
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmErrorCodes.dat	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmErrorCodes.dat	(revision 15358)
@@ -0,0 +1,18 @@
+#
+# This file is used to generate pmErrorCodes.h
+#
+UNKNOWN			Unknown psModules error code
+PHOTOM			Problem in photometry
+PSF			Problem in PSF
+ASTROM			Problem in astrometry
+CAMERA			Problem in camera
+CONCEPTS		Problem in concepts
+IMCOMBINE		Problem in imcombine
+OBJECTS			Problem in objects
+SKY			Problem in sky
+# these errors correspond to standard exit conditions
+ARGUMENTS               Incorrect arguments
+SYS                     System error
+CONFIG                  Problem in configure files
+PROG                    Programming error
+DATA                    invalid data
Index: /branches/eam_branch_20071023/psModules/src/config/pmErrorCodes.h.in
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmErrorCodes.h.in	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmErrorCodes.h.in	(revision 15358)
@@ -0,0 +1,18 @@
+#if !defined(PM_ERROR_CODES_H)
+#define PM_ERROR_CODES_H
+/*
+ * The line
+ *  PM_ERR_$X{ErrorCode},
+ * (without the X)
+ *
+ * will be replaced by values from errorCodes.dat
+ */
+typedef enum {
+    PM_ERR_BASE = 1200,
+    PM_ERR_${ErrorCode},
+    PM_ERR_NERROR
+} pmErrorCode;
+
+void pmErrorRegister(void);
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/config/pmVersion.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmVersion.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmVersion.c	(revision 15358)
@@ -0,0 +1,28 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmVersion.h"
+
+static const char *cvsTag = "$Name: not supported by cvs2svn $";// CVS tag name
+
+psString psModulesVersion(void)
+{
+    psString version = NULL;            // Version, to return
+    psStringAppend(&version, "%s-%s",PACKAGE_NAME,PACKAGE_VERSION);
+    return version;
+}
+
+psString psModulesVersionLong(void)
+{
+    psString version = psModulesVersion(); // Version, to return
+    psString tag = psStringStripCVS(cvsTag, "Name"); // CVS tag
+
+    psStringAppend(&version, " (cvs tag %s) %s, %s", tag, __DATE__, __TIME__);
+
+    psFree(tag);
+    return version;
+}
Index: /branches/eam_branch_20071023/psModules/src/config/pmVersion.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/config/pmVersion.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/config/pmVersion.h	(revision 15358)
@@ -0,0 +1,37 @@
+/*  @file pmVersion.h
+ *  @brief Version functions
+ * 
+ *  @author Paul Price, IfA
+ *  @author Eugene Magnier, IfA
+ * 
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-03-30 21:12:56 $
+ *  Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_VERSION_H
+#define PM_VERSION_H
+
+/// @addtogroup Config Configuration System
+/// @{
+
+/** Get current psModules version
+ *
+ *  Returns the current psModules version name as a string.
+ *
+ *  @return psString: String with version name.
+ */
+psString psModulesVersion(
+    void
+);
+
+/** Get current psModules version (full identification)
+ *
+ *  Returns the current psModules version name and other information identifying the compilation.
+ *
+ *  @return psString: String with identity.
+ */
+psString psModulesVersionLong(void);
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/detrend/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/.cvsignore	(revision 15358)
@@ -0,0 +1,6 @@
+.deps
+.libs
+Makefile
+Makefile.in
+*.la
+*.lo
Index: /branches/eam_branch_20071023/psModules/src/detrend/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/Makefile.am	(revision 15358)
@@ -0,0 +1,33 @@
+noinst_LTLIBRARIES = libpsmodulesdetrend.la
+
+libpsmodulesdetrend_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS)
+libpsmodulesdetrend_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulesdetrend_la_SOURCES  = \
+	pmFlatField.c \
+	pmFlatNormalize.c \
+	pmFringeStats.c \
+	pmMaskBadPixels.c \
+	pmNonLinear.c \
+	pmBias.c \
+	pmOverscan.c \
+	pmDetrendDB.c \
+	pmShutterCorrection.c \
+	pmShifts.c
+
+#	pmSkySubtract.c
+
+pkginclude_HEADERS = \
+	pmFlatField.h \
+	pmFlatNormalize.h \
+	pmFringeStats.h \
+	pmMaskBadPixels.h \
+	pmNonLinear.h \
+	pmBias.h \
+	pmOverscan.h \
+	pmDetrendDB.h \
+	pmShutterCorrection.h \
+	pmShifts.h
+
+#	pmSkySubtract.h
+
+CLEANFILES = *~
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmBias.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmBias.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmBias.c	(revision 15358)
@@ -0,0 +1,193 @@
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPACalibration.h"
+
+#include "pmOverscan.h"
+#include "pmBias.h"
+
+// pmBiasSubtractFrame(): this routine will take as input a readout for the input image and a readout for the bias
+// image.  The bias image is subtracted in place from the input image.
+bool pmBiasSubtractFrame(pmReadout *in, // Input readout
+                          const pmReadout *sub, // Readout to be subtracted from input
+                          float scale   // Scale to apply before subtracting
+                         )
+{
+    PS_ASSERT_PTR_NON_NULL(in, false);
+    PS_ASSERT_PTR_NON_NULL(in->image, false);
+    PS_ASSERT_IMAGE_TYPE(in->image, PS_TYPE_F32, false);
+    PS_ASSERT_IMAGE_NON_EMPTY(in->image, false);
+    PS_ASSERT_PTR_NON_NULL(sub, false);
+    PS_ASSERT_PTR_NON_NULL(sub->image, false);
+    PS_ASSERT_IMAGE_TYPE(sub->image, PS_TYPE_F32, false);
+    PS_ASSERT_IMAGE_NON_EMPTY(sub->image, false);
+    PS_ASSERT_IMAGES_SIZE_EQUAL(in->image, sub->image, false);
+
+    psImage *inImage  = in->image;      // The input image
+    psImage *inMask   = in->mask;       // The input mask
+    psImage *subImage = sub->image;     // The image to be subtracted
+    psImage *subMask  = sub->mask;      // The mask for the subtraction image
+
+    // Check parities
+    int xIpar = psMetadataLookupS32(NULL, in->parent->concepts, "CELL.XPARITY");
+    int xSpar = psMetadataLookupS32(NULL, sub->parent->concepts, "CELL.XPARITY");
+    if (xIpar != xSpar) {
+        psError(PS_ERR_UNKNOWN, true, "images for subtraction do not have the same "
+                "CELL.XPARITY (%d vs %d).\n    pmSubtractBias must be upgraded to handle this situation\n",
+                xIpar, xSpar);
+        return false;
+    }
+
+    int yIpar = psMetadataLookupS32(NULL, in->parent->concepts, "CELL.YPARITY");
+    int ySpar = psMetadataLookupS32(NULL, sub->parent->concepts, "CELL.YPARITY");
+    if (yIpar != ySpar) {
+        psError(PS_ERR_UNKNOWN, true, "images for subtraction do not have the same "
+                "CELL.YPARITY (%d vs %d).\n    pmSubtractBias must be upgraded to handle this situation\n",
+                xIpar, xSpar);
+        return false;
+    }
+
+    // Offsets of the cells
+    int x0in = psMetadataLookupS32(NULL, in->parent->concepts, "CELL.X0");
+    int y0in = psMetadataLookupS32(NULL, in->parent->concepts, "CELL.Y0");
+    int x0sub = psMetadataLookupS32(NULL, sub->parent->concepts, "CELL.X0");
+    int y0sub = psMetadataLookupS32(NULL, sub->parent->concepts, "CELL.Y0");
+
+    if ((inImage->numCols + x0in - x0sub) > subImage->numCols) {
+        psError(PS_ERR_UNKNOWN, true, "Image does not have enough columns for subtraction (%d vs %d).\n",
+                inImage->numCols + x0in - x0sub, subImage->numCols);
+        return false;
+    }
+    if ((inImage->numRows + y0in - y0sub) > subImage->numRows) {
+        psError(PS_ERR_UNKNOWN, true, "Image does not have enough rows for subtraction (%d vs %d).\n",
+                inImage->numRows + y0in - y0sub, subImage->numRows);
+        return false;
+    }
+
+    if (scale == 1.0) {
+        for (int i = 0; i < inImage->numRows; i++) {
+            for (int j = 0; j < inImage->numCols; j++) {
+                inImage->data.F32[i][j] -= subImage->data.F32[i+y0in-y0sub][j+x0in-x0sub];
+                if (inMask && subMask) {
+                    inMask->data.U8[i][j] |= subMask->data.U8[i+y0in-y0sub][j+x0in-x0sub];
+                }
+            }
+        }
+    } else {
+        for (int i = 0; i < inImage->numRows; i++) {
+            for (int j = 0; j < inImage->numCols; j++) {
+                inImage->data.F32[i][j] -= subImage->data.F32[i+y0in-y0sub][j+x0in-x0sub] * scale;
+                if (inMask && subMask) {
+                    inMask->data.U8[i][j] |= subMask->data.U8[i+y0in-y0sub][j+x0in-x0sub];
+                }
+            }
+        }
+    }
+
+    return true;
+}
+
+bool pmBiasSubtract(pmReadout *in, pmOverscanOptions *overscanOpts,
+                    const pmReadout *bias, const pmReadout *dark, const pmFPAview *view)
+{
+    psTrace("psModules.detrend", 4,
+            "---- pmBiasSubtract() begin ----\n");
+
+    PS_ASSERT_PTR_NON_NULL(in, false);
+    PS_ASSERT_IMAGE_NON_NULL(in->image, false);
+    PS_ASSERT_IMAGE_TYPE(in->image, PS_TYPE_F32, false);
+    if (bias) {
+        PS_ASSERT_IMAGE_NON_NULL(bias->image, false);
+        PS_ASSERT_IMAGE_TYPE(bias->image, PS_TYPE_F32, false);
+    }
+    if (dark) {
+        PS_ASSERT_PTR_NON_NULL(view, false);
+        PS_ASSERT_IMAGE_NON_NULL(dark->image, false);
+        PS_ASSERT_IMAGE_TYPE(dark->image, PS_TYPE_F32, false);
+    }
+
+    pmHDU *hdu = pmHDUFromReadout(in);  // HDU of interest
+
+    if (!pmOverscanSubtract (in, overscanOpts)) {
+	return false;
+    }
+
+    // Bias frame subtraction
+    if (bias) {
+        psVector *md5 = psImageMD5(bias->image); // md5 hash
+        psString md5string = psMD5toString(md5); // String
+        psFree(md5);
+        psStringPrepend(&md5string, "BIAS image MD5: ");
+        psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+                         md5string, "");
+        psFree(md5string);
+
+        if (!pmBiasSubtractFrame(in, bias, 1.0)) {
+            return false;
+        }
+    }
+
+    if (dark) {
+        // Get the scaling
+        float inTime = psMetadataLookupF32(NULL, in->parent->concepts, "CELL.DARKTIME");
+        float darkTime = psMetadataLookupF32(NULL, dark->parent->concepts, "CELL.DARKTIME");
+        if (isnan(inTime) || isnan(darkTime)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to determine dark scaling.");
+            return false;
+        }
+
+	float darkNorm = 1.0;
+        float inNorm = pmFPADarkNorm(in->parent->parent->parent, view, inTime);
+
+	// if we have a normalized dark exposure, we simply multiply the master by inNorm.  if
+	// we do not have a normalized exposure, we have to scale the master as well.  XXX do
+	// we need to explicitly identify the master as normalized?
+
+	if (darkTime != 1.0) {
+	    darkNorm = pmFPADarkNorm(dark->parent->parent->parent, view, darkTime);
+	}
+
+	if (isnan(inNorm) || isnan(darkNorm)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to determine dark normalisations.");
+            return false;
+        }
+
+        float scale = inNorm / darkNorm;// Scaling to apply to dark exposure
+
+        psVector *md5 = psImageMD5(dark->image); // md5 hash
+        psString md5string = psMD5toString(md5); // String
+        psFree(md5);
+        psStringPrepend(&md5string, "DARK image (scale %.3f) MD5: ", scale);
+        psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+                         md5string, "");
+        psFree(md5string);
+
+        if (!pmBiasSubtractFrame(in, dark, scale)) {
+            return false;
+        }
+    }
+
+    psTime *time = psTimeGetNow(PS_TIME_TAI); // The time now, used for reporting
+    psString timeString = psTimeToISO(time); // String with time
+    psFree(time);
+    psStringPrepend(&timeString, "Overscan/bias/dark processing completed at ");
+    psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+                     timeString, "");
+    psFree(timeString);
+
+
+    return true;
+}
+
+
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmBias.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmBias.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmBias.h	(revision 15358)
@@ -0,0 +1,37 @@
+/* @file pmBias.h
+ * @brief Subtract the overscan, bias and dark
+ *
+ * @author George Gusciora, MHPCC
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-08-15 20:21:18 $
+ * Copyright 2004--2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_BIAS_H
+#define PM_BIAS_H
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+/// Subtract the overscan, bias and/or dark
+///
+/// Subtracts the overscan, as measured from the bias member of the input readout (if options are non-NULL),
+/// bias (if non-NULL) and dark (if non-NULL) scaled by the CELL.DARKTIME concept.
+bool pmBiasSubtract(pmReadout *in,      ///< Input readout, to be overscan/bias/dark corrected
+                    pmOverscanOptions *overscanOpts, ///< Options for overscan subtraction, or NULL
+                    const pmReadout *bias, ///< Bias to subtract, or NULL
+                    const pmReadout *dark, ///< Dark to scale and subtract, or NULL
+                    const pmFPAview *view ///< View for readout of interest
+                   );
+
+// pmBiasSubtractFrame(): this routine will take as input a readout for the input image and a readout for the bias
+// image.  The bias image is subtracted in place from the input image.
+bool pmBiasSubtractFrame(pmReadout *in, // Input readout
+                          const pmReadout *sub, // Readout to be subtracted from input
+                          float scale   // Scale to apply before subtracting
+    );
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmDetrendDB.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmDetrendDB.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmDetrendDB.c	(revision 15358)
@@ -0,0 +1,308 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmConfigCommand.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmDetrendDB.h"
+#include "psPipe.h"
+#include "psIOBuffer.h"
+
+// ************* detrend select functions **************
+
+//
+static void pmDetrendSelectOptionsFree (pmDetrendSelectOptions *options)
+{
+
+    if (!options)
+        return;
+
+    psFree (options->camera);
+    psFree (options->filter);
+    psFree (options->dettype);
+    psFree (options->version);
+
+    return;
+}
+
+// define basic options for a new detrend database query
+pmDetrendSelectOptions *pmDetrendSelectOptionsAlloc(const char *camera, psTime time, pmDetrendType type)
+{
+
+    pmDetrendSelectOptions *options = psAlloc(sizeof(pmDetrendSelectOptions));
+    psMemSetDeallocator(options, (psFreeFunc) pmDetrendSelectOptionsFree);
+
+    // basic options required by every query
+    options->camera = psStringCopy (camera);
+    options->time   = time;
+    options->type   = type;
+
+    // these other options depend on the type of detrend data
+    options->filter   = NULL;
+    options->version  = NULL;
+    options->dettype  = NULL;
+    options->exptime  = 0.0;
+    options->airmass  = 0.0;
+    options->dettemp  = 0.0;
+    options->twilight = 0.0;
+
+    options->exptimeSet  = false; // not selected
+    options->airmassSet  = false; // not selected
+    options->dettempSet  = false; // not selected
+    options->twilightSet = false; // not selected
+
+    return options;
+}
+
+static void pmDetrendSelectResultsFree (pmDetrendSelectResults *results)
+{
+
+    if (!results)
+        return;
+
+    psFree (results->detID);
+    psFree(results->level);
+
+    return;
+}
+
+pmDetrendSelectResults *pmDetrendSelectResultsAlloc ()
+{
+
+    pmDetrendSelectResults *results = psAlloc(sizeof(pmDetrendSelectResults));
+    psMemSetDeallocator(results, (psFreeFunc) pmDetrendSelectResultsFree);
+
+    results->detID = NULL;
+    results->level = NULL;
+
+    return results;
+}
+
+psString pmDetrendTypeToString (pmDetrendType type)
+{
+
+    # define DETREND_STRING_CASE(TTT) \
+case PM_DETREND_TYPE_##TTT: \
+    return psStringCopy (#TTT);
+
+    switch (type) {
+        DETREND_STRING_CASE (MASK);
+        DETREND_STRING_CASE (BIAS);
+        DETREND_STRING_CASE (DARK);
+        DETREND_STRING_CASE (FLAT);
+        DETREND_STRING_CASE (FLAT_CORRECTION);
+        DETREND_STRING_CASE (SHUTTER);
+        DETREND_STRING_CASE (FRINGE);
+    default:
+        return NULL;
+    }
+    return NULL;
+}
+
+// detselect -camera (camera) -time (time) -type (type) [others]
+// returns: (type) (class) (exp_flag) DONE
+pmDetrendSelectResults *pmDetrendSelect (const pmDetrendSelectOptions *options,
+        const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(options, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(config->site, NULL);
+
+    int status, exit_status;
+    psString line = NULL;
+    char *time = psTimeToISO (&options->time);
+
+    char *type = NULL;
+    if (options->dettype) {
+        type = psMemIncrRefCounter (options->dettype);
+    } else {
+        type = pmDetrendTypeToString (options->type);
+    }
+    unsigned int nFail;
+
+    pmDetrendSelectResults *results = pmDetrendSelectResultsAlloc();
+    psStringAppend(&line, "detselect -search -inst %s -det_type %s -time %s", options->camera, type, time);
+
+    if (options->filter) {
+        psStringAppend(&line, " -filter %s", options->filter);
+    }
+    if (options->version) {
+        psStringAppend(&line, " -version %s", options->version);
+    }
+    if (options->exptimeSet) {
+        psStringAppend(&line, " -exp_time %f", options->exptime);
+    }
+    if (options->airmassSet) {
+        psStringAppend(&line, " -airmass %f", options->airmass);
+    }
+    if (options->dettempSet) {
+        psStringAppend(&line, " -airmass %f", options->dettemp);
+    }
+    if (options->twilightSet) {
+        psStringAppend(&line, " -airmass %f", options->twilight);
+    }
+
+    pmConfigDatabaseCommand(&line, config);
+    pmConfigTraceCommand(&line);
+    psTrace("psModules.detrend", 5, "running %s", line);
+
+    // use psPipe to exec the command, wait for response
+    psIOBuffer *buffer = psIOBufferAlloc (512);
+    psPipe *pipe = psPipeOpen (line);
+    if (!pipe) {
+        psError (PS_ERR_IO, false, "error calling command %s", line);
+        goto failure;
+    }
+
+    status = psIOBufferReadEmpty (buffer, 100, pipe->fd_stdout);
+    if (!status) {
+        psError (PS_ERR_IO, false, "detselect is not responding");
+        goto failure;
+    }
+    exit_status = psPipeClose (pipe);
+    if (exit_status) {
+        psError (PS_ERR_IO, false, "error running detselect");
+        goto failure;
+    }
+
+    psTrace("psModules.detrend", 5, "got answer: %s\n", buffer->data);
+
+    nFail = 0;
+    psMetadata *answer = psMetadataConfigParse (NULL, &nFail, buffer->data, false);
+    if (!answer) {
+        psError(PS_ERR_IO, false, "failed to parse response from detselect\n");
+        psLogMsg ("psModule.detrend", PS_LOG_ERROR, "detselect response (%d bytes):\n %s\n", buffer->n, buffer->data);
+        psLogMsg ("psModule.detrend", PS_LOG_ERROR, "detselect command:\n %s\n", line);
+        goto failure;
+    }
+
+    psMetadata *md = psMetadataLookupPtr (NULL, answer, "detExp");
+    if (!md) {
+        psError(PS_ERR_IO, false, "detselect response is missing 'detExp' Metadata\n");
+        psLogMsg ("psModule.detrend", PS_LOG_ERROR, "detselect response:\n %s\n", buffer->data);
+        goto failure;
+    }
+
+    bool mdstatus;
+    int detID = psMetadataLookupS32 (&mdstatus, md, "det_id");
+    if (!mdstatus) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find det_id in output from detselect.");
+        psLogMsg ("psModule.detrend", PS_LOG_ERROR, "detselect response:\n %s\n", buffer->data);
+        goto failure;
+    }
+    int iteration  = psMetadataLookupS32 (&mdstatus, md, "iteration");
+    if (!mdstatus) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find iteration in output from detselect.");
+        psLogMsg ("psModule.detrend", PS_LOG_ERROR, "detselect response:\n %s\n", buffer->data);
+        goto failure;
+    }
+    psString fileLevel = psMetadataLookupStr(&mdstatus, md, "filelevel");
+    if (!mdstatus || !fileLevel || strlen(fileLevel) == 0) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find filelevel in output from detselect.");
+        psLogMsg ("psModule.detrend", PS_LOG_ERROR, "detselect response:\n %s\n", buffer->data);
+        goto failure;
+    }
+
+    results->detID = NULL; // it should be NULL already from the Alloc above
+    psStringAppend (&results->detID, " -det_id %d -iteration %d ", detID, iteration);
+    results->level = psMemIncrRefCounter(fileLevel);
+
+    psTrace("psModules.detrend", 5, "generated detID %s\n", results->detID);
+
+    psFree (answer);
+    psFree (buffer);
+    psFree (pipe);
+    psFree (line);
+    psFree (time);
+    psFree (type);
+    return results;
+
+failure:
+    psFree (results);
+    psFree (pipe);
+    psFree (buffer);
+    psFree (line);
+    psFree (type);
+    psFree (time);
+    return NULL;
+}
+
+// ************* detrend file functions **************
+
+// detselect -select -detID (detID) -classID (classID)
+// returns: (detID) (classID) (filename) DONE
+char *pmDetrendFile (const char *detID, const char *classID, const pmConfig *config)
+{
+    unsigned int nFail;
+
+    PS_ASSERT_PTR_NON_NULL(detID, NULL);
+
+    bool status;
+    psString line = NULL;
+    psArray *array = NULL;
+
+    // generate the detselect command
+    psStringAppend (&line, "detselect -select %s", detID);
+    if (classID && strlen(classID) > 0) {
+        psStringAppend(&line, " -class_id %s", classID);
+    }
+    pmConfigDatabaseCommand(&line, config);
+    pmConfigTraceCommand(&line);
+    psTrace("psModules.detrend", 5, "running %s", line);
+
+    // use psPipe to exec the command, wait for response
+    psIOBuffer *buffer = psIOBufferAlloc (512);
+    psPipe *pipe = psPipeOpen (line);
+    if (!pipe) {
+        psError (PS_ERR_IO, false, "error calling command %s", line);
+        goto failure;
+    }
+    status = psIOBufferReadEmpty (buffer, 100, pipe->fd_stdout);
+    if (!status) {
+        psError (PS_ERR_IO, false, "detselect is not responding");
+        goto failure;
+    }
+    status = psPipeClose (pipe);
+    if (status) {
+        psError (PS_ERR_IO, false, "error running detselect");
+        goto failure;
+    }
+
+    psTrace("psModules.detrend", 5, "got answer: %s\n", buffer->data);
+
+    psMetadata *answer = psMetadataConfigParse (NULL, &nFail, buffer->data, false);
+    if (!answer) {
+        psError(PS_ERR_IO, false, "failed to parse response from detselect\n");
+        psLogMsg ("psModule.detrend", PS_LOG_ERROR, "detselect response (%d bytes):\n %s\n", buffer->n, buffer->data);
+        goto failure;
+    }
+    psMetadata *md = psMetadataLookupPtr (NULL, answer, "detNormalizedImfile");
+    if (!md) {
+        psError(PS_ERR_IO, false, "detselect response is missing 'detNormalizedImfile' Metadata\n");
+        psLogMsg ("psModule.detrend", PS_LOG_ERROR, "detselect response:\n %s\n", buffer->data);
+        goto failure;
+    }
+
+    char *result = psStringCopy (psMetadataLookupStr (NULL, md, "uri"));
+    psTrace("psModules.detrend", 5, "detrend file: %s\n", result);
+
+    psFree (answer);
+    psFree (pipe);
+    psFree (buffer);
+    psFree (line);
+    return result;
+
+failure:
+    psFree (array);
+    psFree (pipe);
+    psFree (buffer);
+    psFree (line);
+    return NULL;
+}
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmDetrendDB.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmDetrendDB.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmDetrendDB.h	(revision 15358)
@@ -0,0 +1,64 @@
+/* @file  pmDetrendDB.h
+ * @brief Tools to query the detrend database system
+ *
+ * the functions in here do not perform the detrend database queries directly.  all interfaces
+ * to the detrend database go through the external dettools functions.  this allows the modules
+ * and directly dependent program to be sufficiently independent of the database schema that it
+ * can be used with any properly defined detrend database tables.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-06-13 03:42:33 $
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_DETREND_DB_H
+#define PM_DETREND_DB_H
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+typedef enum {
+    PM_DETREND_TYPE_MASK,
+    PM_DETREND_TYPE_BIAS,
+    PM_DETREND_TYPE_DARK,
+    PM_DETREND_TYPE_FLAT,
+    PM_DETREND_TYPE_FLAT_CORRECTION,
+    PM_DETREND_TYPE_SHUTTER,
+    PM_DETREND_TYPE_FRINGE,
+    PM_DETREND_TYPE_BACKGROUND,
+} pmDetrendType;
+
+typedef struct {
+    char *camera;                       // name of camera
+    char *version;                      // optional version string
+    char *filter;                       // name of filter
+    char *dettype;                      // actual detrend type name
+    float exptime;                      // exposure time (for dark, maybe flat & fringe)
+    float airmass;                      // for fringe
+    float dettemp;                      // for fringe
+    float twilight;                     // hours (or seconds?) since/before nearest twilight
+    psTime time;                        // time of input data
+    pmDetrendType type;                 // type of detrend data
+
+    bool  exptimeSet;
+    bool  airmassSet;
+    bool  dettempSet;
+    bool  twilightSet;
+} pmDetrendSelectOptions;
+
+typedef struct {
+    char *detID;                        // identifier of detrend run
+    psString level;                     // level in FPA hierarchy of individual file
+} pmDetrendSelectResults;
+
+psString pmDetrendTypeToString (pmDetrendType type);
+
+pmDetrendSelectOptions *pmDetrendSelectOptionsAlloc(const char *camera, psTime time, pmDetrendType type);
+pmDetrendSelectResults *pmDetrendSelectResultsAlloc();
+pmDetrendSelectResults *pmDetrendSelect (const pmDetrendSelectOptions *options, const pmConfig *config);
+char *pmDetrendFile (const char *detID, const char *classID, const pmConfig *config);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmFlatField.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmFlatField.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmFlatField.c	(revision 15358)
@@ -0,0 +1,118 @@
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+#include "pmFPAMaskWeight.h"
+#include "pmFlatField.h"
+
+bool pmFlatField(pmReadout *in, const pmReadout *flat, psMaskType badFlat)
+{
+    PS_ASSERT_PTR_NON_NULL(in, false);
+    PS_ASSERT_PTR_NON_NULL(in->image, false);
+    PS_ASSERT_IMAGE_NON_EMPTY(in->image, false);
+    PS_ASSERT_PTR_NON_NULL(flat, false);
+    PS_ASSERT_PTR_NON_NULL(flat->image, false);
+    PS_ASSERT_IMAGE_NON_EMPTY(flat->image, false);
+    if (in->mask) {
+        PS_ASSERT_IMAGE_TYPE(in->mask, PS_TYPE_MASK, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(in->mask, in->image, false);
+    }
+    PS_ASSERT_IMAGE_TYPE(flat->image, in->image->type.type, false);
+    if (flat->mask) {
+        PS_ASSERT_IMAGE_TYPE(flat->mask, PS_TYPE_MASK, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(flat->mask, flat->image, false);
+    }
+
+    psImage *inImage   = in->image;     // Input image
+    psImage *inMask    = in->mask;      // Mask for input image
+    psImage *flatImage = flat->image;   // Flat-field image
+    psImage *flatMask  = flat->mask;    // Mask for flat-field image
+
+    // Add flat-field MD5 to header
+    pmHDU *hdu = pmHDUFromReadout(in);  // HDU of interest
+    psVector *md5 = psImageMD5(flat->image); // md5 hash
+    psString md5string = psMD5toString(md5); // String
+    psFree(md5);
+    psStringPrepend(&md5string, "FLAT image MD5: ");
+    psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+                     md5string, "");
+    psFree(md5string);
+
+    // Check input image is not larger than flat image; mask is the same size as the input
+    if (inImage->numRows > flatImage->numRows || inImage->numCols > flatImage->numCols) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true, "Input image (%dx%d) is larger than flat-field image "
+                "(%dx%d).\n", inImage->numCols, inImage->numRows, flatImage->numCols, flatImage->numRows);
+        return false;
+    }
+
+    // Offsets on the chip
+    int x0in = psMetadataLookupS32(NULL, in->parent->concepts, "CELL.X0");
+    int y0in = psMetadataLookupS32(NULL, in->parent->concepts, "CELL.Y0");
+    int x0flat = psMetadataLookupS32(NULL, flat->parent->concepts, "CELL.X0");
+    int y0flat = psMetadataLookupS32(NULL, flat->parent->concepts, "CELL.Y0");
+
+    // Determine offset based on image offset with chip offset: input frame to flat frame
+    int yOffset = in->row0 + y0in - flat->row0 - y0flat;
+    int xOffset = in->col0 + x0in - flat->col0 - x0flat;
+
+    // Check that offsets are within image limits
+    if (inImage->numRows + yOffset > flatImage->numRows ||
+            inImage->numCols + xOffset > flatImage->numCols) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true, "Input image (%dx%d) with offsets (%d,%d) is larger than "
+                "flat-field image (%dx%d).\n", inImage->numCols, inImage->numRows, xOffset, yOffset,
+                flatImage->numCols, flatImage->numRows);
+        return false;
+    }
+
+    // Macro for all PS types
+    #define FLAT_DIVISION_CASE(TYPE, SPECIAL) \
+case PS_TYPE_##TYPE: \
+    for (int j = 0; j < inImage->numRows; j++) { \
+        for (int i = 0; i < inImage->numCols; i++) { \
+            ps##TYPE flatValue = flatImage->data.TYPE[j + yOffset][i + xOffset]; \
+            if (flatValue <= 0.0 || (flatMask && flatMask->data.U8[j + yOffset][i + xOffset])) { \
+                if (inMask) { \
+                    inMask->data.PS_TYPE_MASK_DATA[j][i] |= badFlat; \
+                } \
+                inImage->data.TYPE[j][i] = SPECIAL; \
+            } else { \
+                inImage->data.TYPE[j][i] /= flatValue; \
+            } \
+        } \
+    } \
+    break;
+
+    switch (inImage->type.type) {
+        FLAT_DIVISION_CASE(U8,  0);
+        FLAT_DIVISION_CASE(U16, 0);
+        FLAT_DIVISION_CASE(U32, 0);
+        FLAT_DIVISION_CASE(U64, 0);
+        FLAT_DIVISION_CASE(S8,  0);
+        FLAT_DIVISION_CASE(S16, 0);
+        FLAT_DIVISION_CASE(S32, 0);
+        FLAT_DIVISION_CASE(S64, 0);
+        FLAT_DIVISION_CASE(F32, NAN);
+        FLAT_DIVISION_CASE(F64, NAN);
+    default:
+        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Unsupported type for input image: %x\n",
+                inImage->type.type);
+        return false;
+    }
+
+    psTime *time = psTimeGetNow(PS_TIME_TAI); // The time now, used for reporting
+    psString timeString = psTimeToISO(time); // String with time
+    psFree(time);
+    psStringPrepend(&timeString, "Flat-field processing completed at ");
+    psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+                     timeString, "");
+    psFree(timeString);
+
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmFlatField.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmFlatField.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmFlatField.h	(revision 15358)
@@ -0,0 +1,30 @@
+/* @file pmFlatField.h
+ * @brief Apply flat field calibration
+ *
+ * @author Ross Harman, MHPCC
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.10 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-06-02 03:51:03 $
+ * Copyright 2004-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FLAT_FIELD_H
+#define PM_FLAT_FIELD_H
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+/// Apply flat field calibration to a readout
+///
+/// This function applies the flat field calibration to the input readout.  Support is available for different
+/// image types, though the input and flat images must have the same type.  The relative offsets between the
+/// input and flat images is determined from the readout row0,col0 and the CELL.X0 and CELL.Y0 concepts.
+/// Normalisation of the flat is left as the responsibility of the caller.  Non-positive pixels in the flat
+/// are masked, if there is a mask present in the input readout.
+bool pmFlatField(pmReadout *in,         ///< Readout with input image
+                 const pmReadout *flat,  ///< Readout with flat image
+                 psMaskType badFlat     ///< Mask value to give bad flat pixels
+                );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmFlatNormalize.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmFlatNormalize.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmFlatNormalize.c	(revision 15358)
@@ -0,0 +1,189 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <math.h>
+#include <pslib.h>
+
+#include "pmFlatNormalize.h"
+
+// I'm not sure that many many iterations are required, but rather suspect that the system converges within a
+// few with absolutely no trouble (it *is* over-constrained).  For this reason, I'm putting the maximum number
+// of iterations and tolerance as preset values.
+#define MAXITER 10                      // Maximum number of iterations
+#define TOLERANCE 1e-3                  // Minimum tolerance for convergance
+
+
+bool pmFlatNormalize(psVector **expFluxesPtr, psVector **chipGainsPtr, const psImage *bgMatrix)
+{
+    PS_ASSERT_PTR_NON_NULL(bgMatrix, false);
+    PS_ASSERT_IMAGE_NON_NULL(bgMatrix, false);
+
+    int numExps = bgMatrix->numRows; // Number of exposures
+    int numChips = bgMatrix->numCols; // Number of chips with which each exposure is made
+
+    psVector *expFluxes;                // Dereferenced version of expFluxesPtr
+    if (expFluxesPtr) {
+        if (*expFluxesPtr) {
+            PS_ASSERT_VECTOR_TYPE(*expFluxesPtr, PS_TYPE_F32, false);
+            PS_ASSERT_VECTOR_SIZE(*expFluxesPtr, (long)numExps, false);
+        } else {
+            *expFluxesPtr = psVectorAlloc(numExps, PS_TYPE_F32);
+        }
+        expFluxes = psMemIncrRefCounter(*expFluxesPtr);
+    } else {
+        expFluxes = psVectorAlloc(numExps, PS_TYPE_F32);
+    }
+
+    psVector *chipGains;                // Dereferenced version of chipGainsPtr
+    if (chipGainsPtr) {
+        if (*chipGainsPtr) {
+            PS_ASSERT_VECTOR_TYPE(*chipGainsPtr, PS_TYPE_F32, false);
+            PS_ASSERT_VECTOR_SIZE(*chipGainsPtr, (long)numChips, false);
+        } else {
+            *chipGainsPtr = psVectorAlloc(numChips, PS_TYPE_F32);
+            psVectorInit(*chipGainsPtr, 1.0);
+        }
+        chipGains = psMemIncrRefCounter(*chipGainsPtr);
+    } else {
+        chipGains = psVectorAlloc(numChips, PS_TYPE_F32);
+        psVectorInit(chipGains, 1.0);
+    }
+
+    // Take the logarithms
+    psImage *flux = psImageCopy(NULL, bgMatrix, PS_TYPE_F32); // Copy of the input flux levels matrix
+    psImage *fluxMask = psImageAlloc(numChips, numExps, PS_TYPE_U8); // Mask for bad measurements
+    psImageInit(fluxMask, 0);
+    psVector *gainMask = psVectorAlloc(numChips, PS_TYPE_U8); // Mask for bad gains
+    psVectorInit(gainMask, 0);
+    psVector *expMask = psVectorAlloc(numExps, PS_TYPE_U8); // Mask for bad exposures
+    psVectorInit(expMask, 0);
+    for (int i = 0; i < numChips; i++) {
+        // Note: the input gains are in e/ADU; we want to work with ADU/e (bg [ADU] = g [ADU/e] * f [e])
+        // Hence the minus sign
+        if (isfinite(chipGains->data.F32[i]) && chipGains->data.F32[i] > 0) {
+            chipGains->data.F32[i] = -logf(chipGains->data.F32[i]);
+        } else {
+            chipGains->data.F32[i] = 0.0; // Take a wild guess, gain ~ 1 e/ADU
+        }
+
+        for (int j = 0; j < numExps; j++) {
+            if (isfinite(flux->data.F32[j][i]) && flux->data.F32[j][i] > 0) {
+                flux->data.F32[j][i] = logf(flux->data.F32[j][i]);
+            } else {
+                // Blank out this measurement
+                fluxMask->data.U8[j][i] = 1;
+                flux->data.F32[j][i] = NAN;
+            }
+        }
+    }
+
+    // Not really sure that we need to iterate, but here we go anyway...
+
+    float diff = INFINITY;              // Difference from previous iteration
+    psVector *oldExpFluxes = NULL;      // The fluxes in the previous iteration
+    psVector *oldChipGains = NULL;      // Chip gains in the previous iteration
+    for (int iter = 0; iter < MAXITER && diff > TOLERANCE; iter++) {
+        // Improve on the exposure fluxes
+        int numFluxes = 0;              // Number of fluxes
+        for (int i = 0; i < numExps; i++) {
+            if (expMask->data.U8[i]) {
+                psTrace("psModules.detrend", 7, "Flux for exposure %d is masked.\n", i);
+                continue;
+            }
+            numFluxes++;
+            float sum = 0.0;            // Sum of F_ij - G_j
+            int number = 0;             // Number of chips contributing
+            for (int j = 0; j < numChips; j++) {
+                if (!gainMask->data.U8[j] && !fluxMask->data.U8[i][j]) {
+                    sum += flux->data.F32[i][j] - chipGains->data.F32[j];
+                    number++;
+                }
+            }
+            if (number > 0) {
+                expFluxes->data.F32[i] = sum / (float)number;
+            } else {
+                expMask->data.U8[i] = 1;
+                expFluxes->data.F32[i] = NAN;
+            }
+            psTrace("psModules.detrend", 7, "Flux for exposure %d is %lf\n", i, expf(expFluxes->data.F32[i]));
+        }
+
+        // Improve on the gains
+        float meanGain = 0.0;           // Mean gain
+        int numGains = 0;               // Number of gains
+        for (int i = 0; i < numChips; i++) {
+            if (gainMask->data.U8[i]) {
+                continue;
+            }
+            float sum = 0.0;           // Sum of F_ji - S_j
+            int number = 0;             // Numer of sources contributing
+            for (int j = 0; j < numExps; j++) {
+                if (!fluxMask->data.U8[j][i]) {
+                    sum += flux->data.F32[j][i] - expFluxes->data.F32[j];
+                    number++;
+                }
+            }
+            if (number > 0) {
+                chipGains->data.F32[i] = sum / (float)number;
+            } else {
+                gainMask->data.U8[i] = 1;
+                chipGains->data.F32[i] = NAN;
+            }
+            psTrace("psModules.detrend", 7, "Gain for chip %d is %lf\n", i, expf(-chipGains->data.F32[i]));
+            meanGain += expf(chipGains->data.F32[i]);
+            numGains++;
+        }
+
+        // Normalise the mean gain to unity, and measure the difference
+        meanGain /= (float)numGains;
+        meanGain = logf(meanGain);
+        if (iter > 0) {
+            diff = 0.0;
+            for (int i = 0; i < numChips; i++) {
+                if (gainMask->data.U8[i]) {
+                    continue;
+                }
+                chipGains->data.F32[i] -= meanGain;
+                diff += abs((chipGains->data.F32[i] - oldChipGains->data.F32[i]) / chipGains->data.F32[i]);
+            }
+            for (int i = 0; i < numExps; i++) {
+                if (expMask->data.U8[i]) {
+                    continue;
+                }
+                diff += abs((expFluxes->data.F32[i] - oldExpFluxes->data.F32[i]) / expFluxes->data.F32[i]);
+            }
+        }
+
+        psTrace("psModules.detrend", 2, "Iteration %d: difference is %e\n", iter, diff);
+
+        // Copy the new to the old
+        oldChipGains = psVectorCopy(oldChipGains, chipGains, PS_TYPE_F32);
+        oldExpFluxes = psVectorCopy(oldExpFluxes, expFluxes, PS_TYPE_F32);
+    }
+    psFree(flux);
+    psFree(fluxMask);
+    psFree(oldChipGains);
+    psFree(oldExpFluxes);
+
+    // Un-log the vectors
+    for (int i = 0; i < numChips; i++) {
+        if (!gainMask->data.U8[i]) {
+            chipGains->data.F32[i] = expf(chipGains->data.F32[i]);
+        }
+    }
+    for (int i = 0; i < numExps; i++) {
+        if (!expMask->data.U8[i]) {
+            expFluxes->data.F32[i] = expf(expFluxes->data.F32[i]);
+        }
+    }
+    psFree(gainMask);
+    psFree(expMask);
+
+    psFree(chipGains);
+    psFree(expFluxes);
+
+    return (diff < TOLERANCE); // Did we converge?
+}
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmFlatNormalize.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmFlatNormalize.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmFlatNormalize.h	(revision 15358)
@@ -0,0 +1,31 @@
+/* @file pmFlatNormalize.h
+ * @brief Normalize flat-field measurements
+ *
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2004-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FLAT_NORMALIZE_H
+#define PM_FLAT_NORMALIZE_H
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+/// Normalize flat-field measurements
+///
+/// We have f_ij = g_i s_j where f_ij is the flux recorded for chip i and integration j, g_i is the gain for
+/// the i-th chip, s_j is the flux of the source in the j-th integration.  An initial guess for the chip gains
+/// might be helpful, but is not necessary.  The matrix of background measurements contains the background for
+/// the flat fields used in the combination, as a function of exposure (rows) and chip (columns).  The
+/// exposure fluxes and chip gains are modified upon return with the solved values.  Returns true if the
+/// solution converged.
+bool pmFlatNormalize(psVector **expFluxesPtr, ///< Flux in each exposure, or NULL; modified
+                     psVector **chipGainsPtr, ///< Initial guess of the chip gains or NULL; modified
+                     const psImage *bgMatrix ///< Background measurements: rows are exposures, cols are chips
+                    );
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmFringeStats.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmFringeStats.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmFringeStats.c	(revision 15358)
@@ -0,0 +1,1071 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFringeStats.h"
+
+// Future optimisations for speed:
+//
+// 1. Clipping --- don't re-do the matrix setup again, but carry matrix and vector around, subtract
+// contributions from clipped data points.
+// 2. Faster psImageStats (use memcpy?)
+
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// pmFringeRegions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static void fringeRegionsFree(pmFringeRegions *fringe)
+{
+    psFree(fringe->x);
+    psFree(fringe->y);
+    psFree(fringe->mask);
+    return;
+}
+
+pmFringeRegions *pmFringeRegionsAlloc(int nPts, int dX, int dY, int nX, int nY)
+{
+    pmFringeRegions *fringe = psAlloc(sizeof(pmFringeRegions));
+    (void)psMemSetDeallocator(fringe, (psFreeFunc)fringeRegionsFree);
+
+    fringe->x = NULL;
+    fringe->y = NULL;
+    fringe->mask = NULL;
+
+    fringe->nRequested = nPts;
+    fringe->nAccepted = 0;
+
+    fringe->dX = dX;
+    fringe->dY = dY;
+    fringe->nX = nX;
+    fringe->nY = nY;
+
+    return fringe;
+}
+
+bool pmFringeRegionsCreatePoints(pmFringeRegions *fringe, const psImage *image, psRandom *random)
+{
+    PS_ASSERT_PTR_NON_NULL(fringe, false);
+    PS_ASSERT_PTR_NON_NULL(image, false);
+    PS_ASSERT_IMAGE_NON_EMPTY(image, false);
+
+    double frnd;
+    // create fringe->nRequested
+
+    psRandom *rng;
+    if (random) {
+        rng = psMemIncrRefCounter(random);
+    } else {
+        rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+    }
+
+    fringe->x = psVectorRecycle(fringe->x, fringe->nRequested, PS_TYPE_F32);
+    fringe->y = psVectorRecycle(fringe->y, fringe->nRequested, PS_TYPE_F32);
+    fringe->mask = psVectorRecycle(fringe->mask, fringe->nRequested, PS_TYPE_U8);
+    fringe->x->n = fringe->y->n = fringe->mask->n = fringe->nRequested;
+    psVectorInit(fringe->mask, 0);
+
+    int nX = image->numCols;
+    int nY = image->numRows;
+
+    psF32 *xPt = fringe->x->data.F32;
+    psF32 *yPt = fringe->y->data.F32;
+
+    int dX = fringe->dX;
+    int dY = fringe->dY;
+
+    // generate random points located within image bounds
+    for (int i = 0; i < fringe->nRequested; i++) {
+        frnd = psRandomUniform(rng);
+        xPt[i] = (nX - 2*dX)* frnd + dX;
+        frnd = psRandomUniform(rng);
+        yPt[i] = (nY - 2*dY)* frnd + dY;
+    }
+
+    psFree(rng);
+
+    return true;
+}
+
+bool pmFringeRegionsWriteFits(psFits *fits, psMetadata *header,
+                              const pmFringeRegions *regions, const char *extname)
+{
+    // Make sure the input is well-behaved
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(regions, false);
+    psVector *x = regions->x;           // The x positions
+    psVector *y = regions->y;           // The y positions
+    psVector *mask = regions->mask;     // The region mask
+    int numRows = regions->nRequested;  // Number of rows in the table
+    PS_ASSERT_INT_POSITIVE(numRows, false);
+    PS_ASSERT_VECTOR_NON_NULL(x, false);
+    PS_ASSERT_VECTOR_NON_NULL(y, false);
+    PS_ASSERT_VECTOR_TYPE(x, PS_TYPE_F32, false);
+    PS_ASSERT_VECTOR_TYPE(y, PS_TYPE_F32, false);
+    PS_ASSERT_VECTOR_SIZE(x, (long)numRows, false);
+    PS_ASSERT_VECTOR_SIZE(y, (long)numRows, false);
+    if (mask) {
+        PS_ASSERT_VECTOR_NON_NULL(mask, false);
+        PS_ASSERT_VECTOR_TYPE(mask, PS_TYPE_U8, false);
+        PS_ASSERT_VECTOR_SIZE(mask, (long)numRows, false);
+    }
+
+    // We need to write:
+    // Scalars: dX, dY, nX, nY
+    // Vectors: x, y, mask
+
+    psMetadata *scalars = psMemIncrRefCounter(header); // Metadata to hold the scalars; will be the header
+    if (!scalars) {
+        scalars = psMetadataAlloc();
+    }
+    psMetadataAddS32(scalars, PS_LIST_TAIL, "PSFRNGDX", PS_META_REPLACE, "Median box half-width",
+                     regions->dX);
+    psMetadataAddS32(scalars, PS_LIST_TAIL, "PSFRNGDY", PS_META_REPLACE, "Median box half-height",
+                     regions->dY);
+    psMetadataAddS32(scalars, PS_LIST_TAIL, "PSFRNGNX", PS_META_REPLACE, "Large-scale smoothing in x",
+                     regions->nX);
+    psMetadataAddS32(scalars, PS_LIST_TAIL, "PSFRNGNY", PS_META_REPLACE, "Large-scale smoothing in y",
+                     regions->nY);
+
+    psArray *table = psArrayAlloc(numRows); // The table
+    // Translate the vectors into the required format for psFitsWriteTable()
+    for (long i = 0; i < numRows; i++) {
+        psMetadata *row = psMetadataAlloc();
+        psMetadataAddF32(row, PS_LIST_TAIL, "x", PS_META_REPLACE, "Fringe position in x", x->data.F32[i]);
+        psMetadataAddF32(row, PS_LIST_TAIL, "y", PS_META_REPLACE, "Fringe position in y", y->data.F32[i]);
+        psU8 maskValue = 0;
+        if (mask && mask->data.U8[i]) {
+            maskValue = 0xff;
+        }
+        psMetadataAddU8(row, PS_LIST_TAIL, "mask", PS_META_REPLACE, "Mask", maskValue);
+        table->data[i] = row;
+    }
+
+    bool success;                       // Success of operation
+    if (!(success = psFitsWriteTable(fits, scalars, table, extname))) {
+        psError(PS_ERR_IO, false, "Unable to write fringe data to extension %s\n", extname);
+    }
+    psFree(scalars);
+    psFree(table);
+
+    return success;
+}
+
+pmFringeRegions *pmFringeRegionsReadFits(psMetadata *header, const psFits *fits, const char *extname)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, NULL);
+
+    if (extname && strlen(extname) > 0) {
+        if (!psFitsMoveExtName(fits, extname)) {
+            psError(PS_ERR_IO, false, "Unable to move to extension %s\n", extname);
+            return NULL;
+        }
+    } else if (!psFitsMoveExtNum(fits, 0, false)) {
+        psError(PS_ERR_IO, false, "Unable to move to PHU\n");
+        return NULL;
+    }
+
+    psMetadata *headerCopy = psMemIncrRefCounter(header); // Copy of the header, or NULL
+
+    headerCopy = psFitsReadHeader(headerCopy, fits); // The FITS header
+    if (!headerCopy) {
+        psError(PS_ERR_IO, false, "Unable to read header for extension %s\n", extname);
+        psFree(header);
+        return NULL;
+    }
+
+    // Read the scalars from the header
+    #define READ_SCALAR(SCALAR, NAME) \
+    int SCALAR = psMetadataLookupS32(&mdok, headerCopy, NAME); \
+    if (!mdok || SCALAR <= 0) { \
+        psError(PS_ERR_IO, true, "Unable to find " NAME " in header of extension %s.\n", extname); \
+        psFree(headerCopy); \
+        return NULL; \
+    }
+
+    // Need to retrieve the scalars: dX, dY, nX, nY
+    bool mdok = true;                   // Status of MD lookup
+    READ_SCALAR(dX, "PSFRNGDX");
+    READ_SCALAR(dY, "PSFRNGDY");
+    READ_SCALAR(nX, "PSFRNGNX");
+    READ_SCALAR(nY, "PSFRNGNY");
+    psFree(headerCopy);
+
+    // Now the vectors: x, y, mask
+    psArray *table = psFitsReadTable(fits); // The table
+    long numRows = table->n;            // Number of rows
+
+    pmFringeRegions *regions = pmFringeRegionsAlloc(numRows, dX, dY, nX, nY); // The fringe regions
+    psVector *x = psVectorAlloc(numRows, PS_TYPE_F32); // x position
+    psVector *y = psVectorAlloc(numRows, PS_TYPE_F32); // y position
+    psVector *mask = psVectorAlloc(numRows, PS_TYPE_U8); // mask
+    regions->x = x;
+    regions->y = y;
+    regions->mask = mask;
+
+    #define READ_REGIONS_ROW(VECTOR, TYPE, NAME, DESCRIPTION) \
+    VECTOR->data.TYPE[i] = psMetadataLookup##TYPE(&mdok, row, NAME); \
+    if (!mdok) { \
+        psError(PS_ERR_IO, true, "Unable to find " #DESCRIPTION " .\n"); \
+        psFree(table); \
+        psFree(regions); \
+        return NULL; \
+    }
+
+    // Translate the table into vectors
+    for (long i = 0; i < numRows; i++) {
+        psMetadata *row = table->data[i]; // Table row
+        READ_REGIONS_ROW(x, F32, "x", "x position");
+        READ_REGIONS_ROW(y, F32, "y", "y position");
+        READ_REGIONS_ROW(mask, U8, "mask", "mask");
+    }
+    psFree(table);
+
+    return regions;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// pmFringeStats
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static void fringeStatsFree(pmFringeStats *stats)
+{
+    psFree(stats->regions);
+    psFree(stats->f);
+    psFree(stats->df);
+}
+
+pmFringeStats *pmFringeStatsAlloc(pmFringeRegions *regions)
+{
+    PS_ASSERT_PTR_NON_NULL(regions, false);
+
+    pmFringeStats *stats = psAlloc(sizeof(pmFringeStats));
+    (void)psMemSetDeallocator(stats, (psFreeFunc)fringeStatsFree);
+
+    int numRegions = regions->nRequested; // Number of regions
+    stats->regions = psMemIncrRefCounter(regions);
+    stats->f = psVectorAlloc(numRegions, PS_TYPE_F32);
+    stats->df = psVectorAlloc(numRegions, PS_TYPE_F32);
+
+    return stats;
+}
+
+pmFringeStats *pmFringeStatsMeasure(pmFringeRegions *fringe, const pmReadout *readout, psMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(fringe, NULL);
+    PS_ASSERT_PTR_NON_NULL(readout, NULL);
+    PS_ASSERT_PTR_NON_NULL(readout->image, NULL);
+    PS_ASSERT_IMAGE_NON_EMPTY(readout->image, NULL);
+
+    if (!fringe->x || !fringe->y) {
+        // create the fringe vectors for this image
+        pmFringeRegionsCreatePoints(fringe, readout->image, NULL);
+    }
+
+    PS_ASSERT_PTR_NON_NULL(fringe->x, false);
+    PS_ASSERT_PTR_NON_NULL(fringe->y, false);
+
+    pmFringeStats *measurements = pmFringeStatsAlloc(fringe);
+
+    psF32 *xPt = fringe->x->data.F32;
+    psF32 *yPt = fringe->y->data.F32;
+    psF32 *fPt = measurements->f->data.F32;
+    psF32 *dfPt = measurements->df->data.F32;
+
+    int dX = fringe->dX;
+    int dY = fringe->dY;
+
+    psImage *image = readout->image;
+    psImage *mask  = readout->mask;
+
+    psStats *median = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN); // Median statistics only
+    psStats *medianSd = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV); // Median and SD
+
+    // Measure the sky in each smoothing box
+    psImage *sky = psImageAlloc(fringe->nX, fringe->nY, PS_TYPE_F32);
+    for (int i = 0; i < fringe->nY; i++) {
+        int y0 = image->row0 + (float)i * (float)image->numRows / (float)fringe->nY;
+        int y1 = image->row0 + (float)(i + 1) * (float)image->numRows / (float)fringe->nY;
+        for (int j = 0; j < fringe->nX; j++) {
+            int x0 = image->col0 + (float)j * (float)image->numCols / (float)fringe->nX;
+            int x1 = image->col0 + (float)(j + 1) * (float)image->numCols / (float)fringe->nX;
+            psRegion region = psRegionSet(x0, x1, y0, y1);
+            psImage *subImage = psImageSubset(image, region); // Subimage of the sky region
+            psImage *subMask = NULL;
+            if (mask) {
+                subMask = psImageSubset(mask, region); // Subimage of the sky region
+            }
+            psImageStats(median, subImage, subMask, maskVal);
+            sky->data.F32[i][j] = median->sampleMedian;
+            psFree(subImage);
+            psFree(subMask);
+        }
+    }
+
+    for (int i = 0; i < fringe->x->n; i++) {
+        psRegion region = psRegionSet(image->col0 + xPt[i] - dX,
+                                      image->col0 + xPt[i] + dX + 1,
+                                      image->row0 + yPt[i] - dY,
+                                      image->row0 + yPt[i] + dY + 1);
+        psImage *subImage = psImageSubset(image, region);
+        psImage *subMask = NULL;
+        if (mask) {
+            subMask = psImageSubset(mask, region);
+        }
+        psImageStats(medianSd, subImage, subMask, maskVal);
+        psFree(subImage);
+        psFree(subMask);
+
+        int xSky = xPt[i] / (float)image->numCols * (float)sky->numCols;
+        int ySky = yPt[i] / (float)image->numRows * (float)sky->numRows;
+
+        fPt[i] = medianSd->sampleMedian - sky->data.F32[ySky][xSky];
+        dfPt[i] = 1.0 / medianSd->sampleStdev;
+
+        psTrace("psModules.detrend", 7, "[%d:%d,%d:%d]: %f %f\n", (int)region.x0, (int)region.x1,
+                (int)region.y0, (int)region.y1, fPt[i], dfPt[i]);
+    }
+    psFree(sky);
+    psFree(median);
+    psFree(medianSd);
+
+    return measurements;
+}
+
+bool pmFringeStatsWriteFits(psFits *fits,
+                            psMetadata *header,
+                            const pmFringeStats *fringe,
+                            const char *extname
+                           )
+{
+    // Make sure the input is well-behaved
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(fringe, false);
+    pmFringeRegions *regions = fringe->regions; // The fringe regions
+    PS_ASSERT_PTR_NON_NULL(regions, false);
+    int numRows = regions->nRequested;  // Number of rows in the table
+    PS_ASSERT_INT_POSITIVE(numRows, false);
+    psVector *f = fringe->f;            // The fringe measurements
+    psVector *df = fringe->df;      // The fringe standard deviatiations
+    PS_ASSERT_VECTOR_NON_NULL(f, false);
+    PS_ASSERT_VECTOR_NON_NULL(df, false);
+    PS_ASSERT_VECTOR_TYPE(f, PS_TYPE_F32, false);
+    PS_ASSERT_VECTOR_TYPE(df, PS_TYPE_F32, false);
+    PS_ASSERT_VECTOR_SIZE(f, (long)numRows, false);
+    PS_ASSERT_VECTOR_SIZE(df, (long)numRows, false);
+
+    // We need to write:
+    // Vectors: f, df
+    psArray *table = psArrayAlloc(numRows); // The table
+    // Translate the vectors into the required format for psFitsWriteTable()
+    for (long i = 0; i < numRows; i++) {
+        psMetadata *row = psMetadataAlloc();
+        psMetadataAddF32(row, PS_LIST_TAIL, "f", PS_META_REPLACE, "Fringe measurement", f->data.F32[i]);
+        psMetadataAddF32(row, PS_LIST_TAIL, "df", PS_META_REPLACE, "Fringe stdev", df->data.F32[i]);
+        table->data[i] = row;
+    }
+
+    if (!psFitsWriteTable(fits, header, table, extname)) {
+        psError(PS_ERR_IO, false, "Unable to write fringe data to extension %s\n", extname);
+        psFree(table);
+        return false;
+    }
+
+    psFree(table);
+    return true;
+}
+
+pmFringeStats *pmFringeStatsReadFits(psMetadata *header, const psFits *fits, const char *extname,
+                                     pmFringeRegions *regions)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, NULL);
+    PS_ASSERT_PTR_NON_NULL(regions, NULL);
+    PS_ASSERT_INT_POSITIVE(regions->nRequested, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(regions->x, regions->y, NULL);
+    PS_ASSERT_VECTOR_SIZE(regions->x, (long)regions->nRequested, NULL);
+
+    if (extname && strlen(extname) > 0) {
+        if (!psFitsMoveExtName(fits, extname)) {
+            psError(PS_ERR_IO, false, "Unable to move to extension %s\n", extname);
+            return NULL;
+        }
+    } else if (!psFitsMoveExtNum(fits, 0, false)) {
+        psError(PS_ERR_IO, false, "Unable to move to PHU\n");
+        return NULL;
+    }
+
+    psMetadata *headerCopy = psMemIncrRefCounter(header); // Copy of the header, or NULL
+
+    headerCopy = psFitsReadHeader(headerCopy, fits); // The FITS header
+    if (!headerCopy) {
+        psError(PS_ERR_IO, false, "Unable to read header for extension %s\n", extname);
+        psFree(headerCopy);
+        return NULL;
+    }
+    psFree(headerCopy);
+
+    // Now the vectors: f, df
+    psArray *table = psFitsReadTable(fits); // The table
+    long numRows = table->n;            // Number of rows
+
+    pmFringeStats *fringes = pmFringeStatsAlloc(regions); // The fringe measurements
+    psVector *f = fringes->f;           // fringe measurement
+    psVector *df = fringes->df;         // fringe stdev
+
+    #define READ_STATS_ROW(VECTOR, TYPE, NAME, DESCRIPTION) \
+    VECTOR->data.TYPE[i] = psMetadataLookup##TYPE(&mdok, row, NAME); \
+    if (!mdok) { \
+        psError(PS_ERR_IO, true, "Unable to find " #DESCRIPTION " .\n"); \
+        psFree(table); \
+        psFree(fringes); \
+        return NULL; \
+    }
+
+    // Translate the table into vectors
+    bool mdok;                          // Status of MD lookup
+    for (long i = 0; i < numRows; i++) {
+        psMetadata *row = table->data[i]; // Table row
+        READ_STATS_ROW(f, F32, "f", "fringe measurement");
+        READ_STATS_ROW(df, F32, "df", "fringe standard deviation");
+    }
+    psFree(table);
+
+    return fringes;
+}
+
+
+pmFringeStats *pmFringeStatsConcatenate(const psArray *fringes, const psVector *x0, const psVector *y0)
+{
+    PS_ASSERT_PTR_NON_NULL(fringes, NULL);
+    PS_ASSERT_PTR_NON_NULL(fringes->data, NULL);
+    PS_ASSERT_INT_POSITIVE(fringes->n, NULL);
+    if (x0 && y0) {
+        PS_ASSERT_VECTOR_NON_NULL(x0, NULL);
+        PS_ASSERT_VECTOR_NON_NULL(y0, NULL);
+        PS_ASSERT_VECTOR_TYPE(x0, PS_TYPE_S32, NULL);
+        PS_ASSERT_VECTOR_TYPE(y0, PS_TYPE_S32, NULL);
+        PS_ASSERT_VECTORS_SIZE_EQUAL(x0, y0, NULL);
+        PS_ASSERT_VECTOR_SIZE(x0, fringes->n, NULL);
+        PS_ASSERT_VECTOR_SIZE(y0, fringes->n, NULL);
+    }
+
+    // Get the measurement parameters, and check they are consistent
+    int numPoints = 0;                  // Number of fringe points
+    int dX = 0, dY = 0;                 // Half-width and -height of fringe boxes
+    int nX = 0, nY = 0;                 // Smoothing scales
+    for (long i = 0; i < fringes->n; i++) {
+        pmFringeStats *fringe = fringes->data[i]; // The fringe of interest
+        pmFringeRegions *regions = fringe->regions; // The fringe regions
+        if (numPoints == 0) {
+            dX = regions->dX;
+            dY = regions->dY;
+            nX = regions->nX;
+            nY = regions->nY;
+        } else if (regions->dX != dX || regions->dY != dY || regions->nX != nX || regions->nY != nY) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Fringe %ld has different parameters (%d,%d,%d,%d) "
+                    "from the first (%d,%d,%d,%d).\n", i,
+                    regions->dX, regions->dY, regions->nX, regions->nY, dX, dY, nX, nY);
+            return NULL;
+        }
+        int num = regions->nRequested;  // Number of fringe points
+        if (regions->x->n != num || regions->y->n != num || regions->mask->n != num ||
+                fringe->f->n != num || fringe->df->n != num) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Length of region (%ld,%ld,%ld) and fringe vectors "
+                    "do not match (%ld,%ld) with the official value (%d).\n", regions->x->n, regions->y->n,
+                    regions->mask->n, fringe->f->n, fringe->df->n, num);
+            return NULL;
+        }
+        numPoints += regions->nRequested;
+    }
+
+    pmFringeRegions *newRegions = pmFringeRegionsAlloc(numPoints, dX, dY, nX, nY); // The new list of regions
+    newRegions->x = psVectorAlloc(numPoints, PS_TYPE_F32);
+    newRegions->y = psVectorAlloc(numPoints, PS_TYPE_F32);
+    newRegions->mask = psVectorAlloc(numPoints, PS_TYPE_U8);
+    pmFringeStats *newStats = pmFringeStatsAlloc(newRegions); // The new list of statistics
+
+    long offset = 0;                    // Offset from start of the list
+    for (long i = 0; i < fringes->n; i++) {
+        pmFringeStats *fringe = fringes->data[i]; // The fringe of interest
+        pmFringeRegions *regions = fringe->regions; // The fringe regions
+        // Copy the data over
+        memcpy(&newRegions->x->data.F32[offset], regions->x->data.F32, regions->x->n * sizeof(psF32));
+        memcpy(&newRegions->y->data.F32[offset], regions->y->data.F32, regions->y->n * sizeof(psF32));
+        memcpy(&newRegions->mask->data.U8[offset], regions->mask->data.U8, regions->mask->n * sizeof(psU8));
+        memcpy(&newStats->f->data.F32[offset], fringe->f->data.F32, fringe->f->n * sizeof(psF32));
+        memcpy(&newStats->df->data.F32[offset], fringe->df->data.F32, fringe->df->n * sizeof(psF32));
+        if (x0 && y0) {
+            for (long j = offset; j < offset + regions->x->n; j++) {
+                newRegions->x->data.F32[j] += x0->data.S32[i];
+                newRegions->y->data.F32[j] += y0->data.S32[i];
+            }
+        }
+        offset += regions->nRequested;
+    }
+
+    psFree(newRegions);                 // Drop reference
+    return newStats;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// pmFringeIO
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmFringesFormat(pmCell *cell, psMetadata *header, const psArray *fringes)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_ARRAY_NON_NULL(fringes, false);
+
+    // Check the regions are all identical
+    pmFringeRegions *regions = ((pmFringeStats*)fringes->data[0])->regions; // First region
+    for (int i = 1; i < fringes->n; i++) {
+        pmFringeStats *stats = fringes->data[i];
+        if (stats->regions != regions) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Regions for fringe statistics are not identical.\n");
+            return false;
+        }
+    }
+
+    // Ensure the region is legit
+    psVector *x = regions->x;           // The x positions
+    psVector *y = regions->y;           // The y positions
+    psVector *mask = regions->mask;     // The region mask
+    int numRows = regions->nRequested;  // Number of rows in the table
+    PS_ASSERT_INT_POSITIVE(numRows, false);
+    PS_ASSERT_VECTOR_NON_NULL(x, false);
+    PS_ASSERT_VECTOR_NON_NULL(y, false);
+    PS_ASSERT_VECTOR_TYPE(x, PS_TYPE_F32, false);
+    PS_ASSERT_VECTOR_TYPE(y, PS_TYPE_F32, false);
+    PS_ASSERT_VECTOR_SIZE(x, (long)numRows, false);
+    PS_ASSERT_VECTOR_SIZE(y, (long)numRows, false);
+    if (mask) {
+        PS_ASSERT_VECTOR_NON_NULL(mask, false);
+        PS_ASSERT_VECTOR_TYPE(mask, PS_TYPE_U8, false);
+        PS_ASSERT_VECTOR_SIZE(mask, (long)numRows, false);
+    }
+
+    // We need to write:
+    // Scalars: dX, dY, nX, nY
+    // Vectors: x, y, mask, f, df
+
+    psMetadata *scalars = psMemIncrRefCounter(header); // Metadata to hold the scalars; will be the header
+    if (!scalars) {
+        scalars = psMetadataAlloc();
+    }
+    psMetadataAddS32(scalars, PS_LIST_TAIL, "PSFRNGDX", PS_META_REPLACE, "Median box half-width",
+                     regions->dX);
+    psMetadataAddS32(scalars, PS_LIST_TAIL, "PSFRNGDY", PS_META_REPLACE, "Median box half-height",
+                     regions->dY);
+    psMetadataAddS32(scalars, PS_LIST_TAIL, "PSFRNGNX", PS_META_REPLACE, "Large-scale smoothing in x",
+                     regions->nX);
+    psMetadataAddS32(scalars, PS_LIST_TAIL, "PSFRNGNY", PS_META_REPLACE, "Large-scale smoothing in y",
+                     regions->nY);
+    psMetadataAdd(cell->analysis, PS_LIST_TAIL, "FRINGE.HEADER", PS_DATA_METADATA,
+                  "Header for fringe data", scalars);
+    psFree(scalars);
+
+
+    psArray *table = psArrayAlloc(numRows); // The table
+    // Translate the vectors into the required format for psFitsWriteTable()
+    for (long i = 0; i < numRows; i++) {
+        psMetadata *row = psMetadataAlloc();
+        psMetadataAddF32(row, PS_LIST_TAIL, "x", PS_META_REPLACE, "Fringe position in x", x->data.F32[i]);
+        psMetadataAddF32(row, PS_LIST_TAIL, "y", PS_META_REPLACE, "Fringe position in y", y->data.F32[i]);
+        psU8 maskValue = 0;             // Mask value
+        if (mask && mask->data.U8[i]) {
+            maskValue = 0xff;
+        }
+
+        psVector *f = psVectorAlloc(fringes->n, PS_TYPE_F32); // Measurements for each fringe component
+        psVector *df = psVectorAlloc(fringes->n, PS_TYPE_F32); // Errors in measurements
+        for (long j = 0; j < fringes->n; j++) {
+            pmFringeStats *stats = fringes->data[j]; // Fringe statistics of interest
+            f->data.F32[j] = stats->f->data.F32[i];
+            df->data.F32[j] = stats->df->data.F32[i];
+            if (!isfinite(f->data.F32[j]) || !isfinite(df->data.F32[j])) {
+                maskValue = 0xff;
+            }
+        }
+        psMetadataAdd(row, PS_LIST_TAIL, "f", PS_DATA_VECTOR | PS_META_REPLACE, "Fringe measurements", f);
+        psMetadataAdd(row, PS_LIST_TAIL, "df", PS_DATA_VECTOR | PS_META_REPLACE, "Fringe errors", df);
+        // Drop references
+        psFree(f);
+        psFree(df);
+
+        psMetadataAddU8(row, PS_LIST_TAIL, "mask", PS_META_REPLACE, "Mask", maskValue);
+        table->data[i] = row;
+    }
+
+    psMetadataAdd(cell->analysis, PS_LIST_TAIL, "FRINGE", PS_DATA_ARRAY, "Fringe data", table);
+
+    return true;
+}
+
+
+psArray *pmFringesParse(pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+
+    bool mdok;                          // Status of MD lookup
+    psMetadata *header = psMetadataLookupMetadata(&mdok, cell->analysis, "FRINGE.HEADER"); // Header
+    if (!mdok || !header) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find header for fringe data.\n");
+        return NULL;
+    }
+
+    psArray *table = psMetadataLookupPtr(NULL, cell->analysis, "FRINGE"); // FITS table
+    if (!table) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find table for fringe data.\n");
+        return NULL;
+    }
+
+
+    // Read the scalars from the header
+    #define READ_FRINGES_SCALAR(SCALAR, NAME) \
+    int SCALAR = psMetadataLookupS32(&mdok, header, NAME); \
+    if (!mdok || SCALAR <= 0) { \
+        psError(PS_ERR_IO, true, "Unable to find " NAME " in fringe header.\n"); \
+        return NULL; \
+    }
+
+    // Need to retrieve the scalars: dX, dY, nX, nY
+    READ_FRINGES_SCALAR(dX, "PSFRNGDX");
+    READ_FRINGES_SCALAR(dY, "PSFRNGDY");
+    READ_FRINGES_SCALAR(nX, "PSFRNGNX");
+    READ_FRINGES_SCALAR(nY, "PSFRNGNY");
+
+    // Now the vectors: x, y, mask, f, df
+    long numRows = table->n;            // Number of rows
+    pmFringeRegions *regions = pmFringeRegionsAlloc(numRows, dX, dY, nX, nY); // The fringe regions
+    psVector *x = psVectorAlloc(numRows, PS_TYPE_F32); // x position
+    psVector *y = psVectorAlloc(numRows, PS_TYPE_F32); // y position
+    psVector *mask = psVectorAlloc(numRows, PS_TYPE_U8); // mask
+    regions->x = x;
+    regions->y = y;
+    regions->mask = mask;
+    psArray *f = psArrayAlloc(numRows); // Array of fringe measurements
+    psArray *df = psArrayAlloc(numRows);// Array of errors
+    psArray *fringes = NULL; // Array of fringes, to return
+
+    #define READ_FRINGES_VECTOR_ROW(VECTOR, TYPE, NAME, DESCRIPTION) \
+    { \
+        VECTOR->data.TYPE[i] = psMetadataLookup##TYPE(&mdok, row, NAME); \
+        if (!mdok) { \
+            psError(PS_ERR_IO, true, "Unable to find " #DESCRIPTION " for row %ld.\n", i); \
+            goto READ_FRINGES_DONE; \
+        } \
+    }
+
+    // Some values may be either a vector or a value --- need to check
+    #define READ_FRINGES_ARRAY_ROW(ARRAY, TYPE, NAME, DESCRIPTION) \
+    { \
+        psMetadataItem *item = psMetadataLookup(row, NAME); \
+        if (!item) { \
+            psError(PS_ERR_IO, true, "Unable to find " #DESCRIPTION " for row %ld.\n", i); \
+            goto READ_FRINGES_DONE; \
+        } \
+        if (item->type == PS_DATA_VECTOR) { \
+            ARRAY->data[i] = psMemIncrRefCounter(item->data.V); \
+        } else if (item->type == PS_TYPE_##TYPE) { \
+            psVector *vector = psVectorAlloc(1, PS_TYPE_##TYPE); \
+            vector->data.TYPE[0] = item->data.TYPE; \
+            ARRAY->data[i] = vector; \
+        } else { \
+            psError(PS_ERR_IO, true, "Found " #DESCRIPTION " for row %ld, but it's of an " \
+                    "unsupported type (%x).\n", i, item->type); \
+            goto READ_FRINGES_DONE; \
+        } \
+    }
+
+    // Translate the table into vectors
+    for (long i = 0; i < numRows; i++) {
+        psMetadata *row = table->data[i]; // Table row
+        READ_FRINGES_VECTOR_ROW(x, F32, "x", "x position");
+        READ_FRINGES_VECTOR_ROW(y, F32, "y", "y position");
+        READ_FRINGES_VECTOR_ROW(mask, U8, "mask", "mask");
+        READ_FRINGES_ARRAY_ROW(f, F32, "f", "fringe measurement");
+        READ_FRINGES_ARRAY_ROW(df, F32, "df", "fringe error");
+    }
+
+    // Get f,df into pmFringeStats
+    long numFringes = ((psVector*)(f->data[0]))->n; // Number of fringe components
+    fringes = psArrayAlloc(numFringes);
+    for (int j = 0; j < numFringes; j++) {
+        fringes->data[j] = pmFringeStatsAlloc(regions);
+    }
+
+    for (long i = 0; i < numRows; i++) {
+        psVector *measurements = f->data[i]; // Vector of measurements
+        psVector *errors = df->data[i]; // Vector of errors
+        for (int j = 0; j < numFringes; j++) {
+            pmFringeStats *fringe = fringes->data[j];
+            fringe->f->data.F32[i] = measurements->data.F32[j];
+            fringe->df->data.F32[i] = errors->data.F32[j];
+        }
+    }
+
+READ_FRINGES_DONE:
+    psFree(regions);
+    psFree(f);
+    psFree(df);
+
+    return fringes;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// pmFringeScale
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static void fringeScaleFree(pmFringeScale *scale)
+{
+    psFree(scale->coeff);
+    psFree(scale->coeffErr);
+    return;
+}
+
+pmFringeScale *pmFringeScaleAlloc(int nFringeFrames)
+{
+    pmFringeScale *scale = psAlloc(sizeof(pmFringeScale));
+    (void)psMemSetDeallocator(scale, (psFreeFunc)fringeScaleFree);
+
+    scale->nFringeFrames = nFringeFrames;
+    scale->coeff = psVectorAlloc(nFringeFrames + 1, PS_TYPE_F32);
+    scale->coeffErr = psVectorAlloc(nFringeFrames + 1, PS_TYPE_F32);
+
+    return scale;
+}
+
+// Determine the fringe scales through solving the least-squares problem
+static bool scaleMeasure(pmFringeScale *scale, // Scale to return
+                         pmFringeStats *science, // The fringe measurements for the science image
+                         psArray *fringes // Array of fringe measurements for the templates
+                        )
+{
+    assert(scale);
+    assert(science);
+    assert(fringes);
+    assert(scale->nFringeFrames == fringes->n);
+
+    psVector *mask = science->regions->mask; // The region mask
+
+    int numCoeffs = fringes->n + 1;     // Number of coefficients: scales for the templates plus a background
+    int numPoints = science->regions->nRequested; // Number of points (i.e., fringe measurements)
+
+    psImage *A = psImageAlloc(numCoeffs, numCoeffs, PS_TYPE_F64); // The least-squares matrix
+    psVector *B = psVectorAlloc(numCoeffs, PS_TYPE_F64); // The least-squares vector
+
+    // Generate the least-squares matrix and vector
+    for (int i = 0; i < numCoeffs; i++) {
+        psVector *fringe1 = NULL;       // A fringe measurement
+        if (i != 0) {
+            pmFringeStats *fringe = fringes->data[i - 1];
+            fringe1 = fringe->f;
+        }
+
+        // Fill in the upper part of the matrix
+        for (int j = i; j < numCoeffs; j++) {
+            psVector *fringe2 = NULL;   // Another fringe measurement
+            if (j != 0) {
+                pmFringeStats *fringe = fringes->data[j - 1];
+                fringe2 = fringe->f;
+            }
+
+            double matrix = 0.0;        // The matrix sum
+            for (int k = 0; k < numPoints; k++) {
+                if (!mask->data.U8[k]) {
+                    psF32 f1 = (fringe1) ? fringe1->data.F32[k] : 1.0; // Contribution from i fringe
+                    psF32 f2 = (fringe2) ? fringe2->data.F32[k] : 1.0; // Contribution from j fringe
+                    psF32 dsInv = science->df->data.F32[k]; // 1 / sigma
+                    matrix += f1 * f2 * dsInv * dsInv;
+                }
+            }
+            A->data.F64[i][j] = matrix;
+        }
+
+        // Use symmetry to fill in the lower part of the matrix
+        for (int j = 0; j < i; j++) {
+            A->data.F64[i][j] = A->data.F64[j][i];
+        }
+
+        double vector = 0.0;            // The vector sum
+        for (int k = 0; k < numPoints; k++) {
+            if (!mask->data.U8[k]) {
+                psF32 f1 = (fringe1) ? fringe1->data.F32[k] : 1.0; // Contribution from fringe 1
+                psF32 s = science->f->data.F32[k]; // Contribution from science measurement
+                psF32 dsInv = science->df->data.F32[k]; // 1 / sigma
+                vector += f1 * s * dsInv * dsInv;
+            }
+        }
+        B->data.F64[i] = vector;
+    }
+
+    if (psTraceGetLevel("psModules.detrend") >= 5) {
+        printf("From %d points:\n", numPoints);
+        for (int i = 0; i < numCoeffs; i++) {
+            for (int j = 0; j < numCoeffs; j++) {
+                printf("%.2e ", A->data.F64[i][j]);
+            }
+            printf("\n");
+        }
+    }
+
+    // Solve the least-squares equation
+    if (! psMatrixGJSolve(A, B)) {
+        psError(PS_ERR_UNKNOWN, false, "Could not solve linear equations.  Returning NULL.\n");
+        return false;
+    }
+
+    // Copy the results over
+    for (int i = 0; i < numCoeffs; i++) {
+        scale->coeff->data.F32[i] = B->data.F64[i];
+        scale->coeffErr->data.F32[i] = sqrt(A->data.F64[i][i]);
+    }
+
+    psFree(A);
+    psFree(B);
+
+    return true;
+}
+
+// Measure the fringe differences for each region
+static bool fringeScaleDiffs(psVector *diff, // Vector of differences
+                             pmFringeStats *science, // Science fringe measurements
+                             psArray *fringes, // Template fringe measurements
+                             pmFringeScale *scale // Fringe scales
+                            )
+{
+    assert(diff);
+    assert(diff->type.type == PS_TYPE_F32);
+    assert(science);
+    assert(fringes);
+    assert(scale);
+    assert(diff->n == science->regions->nRequested);
+    assert(fringes->n == scale->nFringeFrames);
+
+    psVector *mask = science->regions->mask; // The region mask
+
+    for (int i = 0; i < diff->n; i++) {
+        if (!mask->data.U8[i]) {
+            float difference = science->f->data.F32[i] - scale->coeff->data.F32[0];
+            for (int j = 0; j < fringes->n; j++) {
+                pmFringeStats *fringe = fringes->data[j]; // The fringe of interest
+                difference -= scale->coeff->data.F32[j + 1] * fringe->f->data.F32[i];
+            }
+            diff->data.F32[i] = difference * difference * science->df->data.F32[i] * science->df->data.F32[i];
+        }
+    }
+
+    return true;
+}
+
+// Clip regions based on the differences; return the number masked
+static int clipRegions(psVector *diffs, // Differences
+                       psVector *mask,  // Region mask
+                       float rej        // Rejection limit in standard deviations
+                      )
+{
+    assert(diffs);
+    assert(diffs->type.type == PS_TYPE_F32);
+    assert(mask);
+    assert(mask->type.type == PS_TYPE_U8);
+    assert(diffs->n == mask->n);
+
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_QUARTILE); // Statistics
+    psVectorStats(stats, diffs, NULL, mask, 1);
+    float middle = stats->sampleMedian; // The middle of the distribution
+    float thresh = rej * 0.74 * (stats->sampleUQ - stats->sampleLQ); // The rejection threshold
+    psFree(stats);
+
+    int numClipped = 0;                 // Number clipped
+    for (int i = 0; i < diffs->n; i++) {
+        psTrace("psModules.detrend", 10, "Region %d (%d): %f\n", i, mask->data.U8[i], diffs->data.F32[i]);
+        if (!mask->data.U8[i] && fabs(diffs->data.F32[i]) > middle + thresh) {
+            psTrace("psModules.detrend", 5, "Masking %d: %f\n", i, diffs->data.F32[i]);
+            mask->data.U8[i] = 1;
+            numClipped++;
+        }
+    }
+
+    return numClipped;
+}
+
+
+// XXX include the fringe error (fringe->df) in the fit?
+pmFringeScale *pmFringeScaleMeasure(pmFringeStats *science, psArray *fringes, float rej,
+                                    unsigned int nIter, float keepFrac)
+{
+    PS_ASSERT_PTR_NON_NULL(science, NULL);
+    PS_ASSERT_PTR_NON_NULL(fringes, NULL);
+    PS_ASSERT_INT_POSITIVE(fringes->n, NULL);
+    PS_ASSERT_INT_POSITIVE(nIter, NULL);
+
+    pmFringeRegions *regions = science->regions; // The fringe regions
+    int numRegions = regions->nRequested; // Number of regions
+
+    // Ensure we are dealing with the SAME fringe points for all the inputs.
+    // Otherwise, we're going to get crazy results.
+    for (long i = 0; i < numRegions; i++) {
+        float xScience = regions->x->data.F32[i]; // The x position for the science image
+        float yScience = regions->y->data.F32[i]; // The y position for the science image
+        for (long j = 0; j < fringes->n; j++) {
+            pmFringeStats *fringe = fringes->data[j]; // The fringe statistics from a fringe image
+            pmFringeRegions *fringeRegions = fringe->regions; // The fringe regions for that fringe image
+            if (fringeRegions->x->data.F32[i] != xScience ||
+                    fringeRegions->y->data.F32[i] != yScience) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Science and fringe measurement regions "
+                        "don't match.\n");
+                return NULL;
+            }
+        }
+    }
+
+    // Set up the mask
+    if (!regions->mask) {
+        regions->mask = psVectorAlloc(numRegions, PS_TYPE_U8);
+        psVectorInit(regions->mask, 0);
+    }
+    psVector *mask = regions->mask;     // The region mask
+    psStats *median = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN); // Median statistics
+    unsigned int numClipped = 0;        // Total number clipped
+    psVector *diff = psVectorAlloc(numRegions, PS_TYPE_F32); // The differences between obs. and pred.
+
+    pmFringeScale *scale = pmFringeScaleAlloc(fringes->n); // The fringe scales
+
+    // Get rid of bad data points
+    for (int i = 0; i < fringes->n; i++) {
+        pmFringeStats *fringe = fringes->data[i]; // The fringe of interest
+        for (int j = 0; j < numRegions; j++) {
+            if (!isfinite(fringe->f->data.F32[j])) {
+                mask->data.U8[j] = 1;
+                psTrace("psModules.detrend", 9, "Masking region %d because not finite in fringe %d.\n", j, i);
+            }
+        }
+    }
+
+# if (0)
+    // Write fringe data to file for a test
+    FILE *f = fopen ("fringe.dat", "w");
+    for (int j = 0; j < numRegions; j++) {
+	if (mask->data.U8[j]) continue;
+	fprintf (f, "%d %f %f ", j, science->f->data.F32[j], science->df->data.F32[j]);
+	for (int i = 0; i < fringes->n; i++) {
+	    pmFringeStats *fringe = fringes->data[i]; // The fringe of interest
+            fprintf (f, "%f  ", fringe->f->data.F32[j]);
+        }
+	fprintf (f, "\n");
+    }
+    fclose (f);
+# endif
+
+    // Get rid of the extreme outliers by assuming most of the points are somewhat clustered
+    psVectorStats(median, science->f, NULL, NULL, 0);
+    scale->coeff->data.F32[0] = median->sampleMedian;
+    for (int i = 0; i < fringes->n; i++) {
+        pmFringeStats *fringe = fringes->data[i]; // The fringe of interest
+        psVectorStats(median, fringe->f, NULL, NULL, 0);
+        scale->coeff->data.F32[0] -= median->sampleMedian;
+        scale->coeff->data.F32[i] = 0.0;
+    }
+    psFree(median);
+    fringeScaleDiffs(diff, science, fringes, scale);
+    numClipped = clipRegions(diff, mask, 3.0*rej);
+    psTrace("psModules.detrend", 4, "%d regions clipped in initial pass.\n", numClipped);
+
+    unsigned int iter = 0;              // Iteration number
+    unsigned int iterClip = 0;          // Number clipped in this iteration
+    do {
+        iter++;
+        scaleMeasure(scale, science, fringes); // The scales
+        psTrace("psModules.detrend", 1, "Fringe scales after iteration %d:\n", iter);
+        psTrace("psModules.detrend", 1, "Background: %f %f\n", scale->coeff->data.F32[0],
+                scale->coeffErr->data.F32[0]);
+        for (int i = 0; i < scale->nFringeFrames; i++) {
+            psTrace("psModules.detrend", 1, "%d: %f %f\n", i, scale->coeff->data.F32[i + 1],
+                    scale->coeffErr->data.F32[i + 1]);
+        }
+
+        fringeScaleDiffs(diff, science, fringes, scale);
+        iterClip = clipRegions(diff, mask, rej); // Number clipped
+        numClipped += iterClip;
+        psTrace("psModules.detrend", 9, "Clipped: %d\tFrac: %f\n", iterClip,
+                (float)numClipped/(float)numRegions);
+    } while (iterClip > 0 && iter < nIter && (float)numClipped/(float)numRegions <= 1.0 - keepFrac);
+    psFree(diff);
+
+    // A final iteration with the last clipping
+    scaleMeasure(scale, science, fringes);
+
+    return scale;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Fringe correction
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// XXX note that this modifies the input fringe images
+psImage *pmFringeCorrect(pmReadout *readout, pmFringeRegions *fringes, psArray *fringeImages,
+                         psArray *fringeStats, psMaskType maskVal, float rej,
+                         unsigned int nIter, float keepFrac)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, NULL);
+    PS_ASSERT_PTR_NON_NULL(readout->image, NULL);
+    PS_ASSERT_IMAGE_NON_EMPTY(readout->image, NULL);
+    PS_ASSERT_PTR_NON_NULL(fringes, NULL);
+    PS_ASSERT_PTR_NON_NULL(fringeImages, NULL);
+    PS_ASSERT_PTR_NON_NULL(fringeStats, NULL);
+    PS_ASSERT_INT_EQUAL(fringeImages->n, fringeStats->n, NULL);
+    PS_ASSERT_INT_POSITIVE(nIter, NULL);
+
+    // measure the fringe stats for the science frame and solve for the scales
+    pmFringeStats *scienceStats = pmFringeStatsMeasure(fringes, readout, maskVal);
+
+    if (psTraceGetLevel("psModules.detrend") > 9) {
+        for (int i = 0; i < fringes->nRequested; i++) {
+            printf("%f", scienceStats->f->data.F32[i]);
+            for (int j = 0; j < fringeStats->n; j++) {
+                pmFringeStats *fringe = fringeStats->data[j];
+                printf("\t%f", fringe->f->data.F32[i]);
+            }
+            printf("\n");
+        }
+    }
+
+    pmFringeScale *scale = pmFringeScaleMeasure(scienceStats, fringeStats, rej, nIter, keepFrac);
+    psFree(scienceStats);
+
+    psTrace("psModules.detrend", 7, "Fringe solution:\n");
+    for (int i = 0; i < fringeImages->n + 1; i++) {
+        psTrace("psModules.detrend", 7, "%d: %f %f\n", i, scale->coeff->data.F32[i],
+                scale->coeffErr->data.F32[i]);
+    }
+
+    // build the fringe correction image
+    // XXX we could save data space by making the first image the output image
+    psImage *sumFringe = psImageAlloc(readout->image->numCols, readout->image->numRows, PS_TYPE_F32);
+    //psBinaryOp(sumFringe, sumFringe, "+", psScalarAlloc(scale->coeff->data.F32[0], PS_TYPE_F32));
+    for (int i = 0; i < fringeImages->n; i++) {
+
+        // rescale the fringe image
+        psBinaryOp(fringeImages->data[i], fringeImages->data[i], "*",
+                   psScalarAlloc(scale->coeff->data.F32[i+1], PS_TYPE_F32));
+
+        // sum together
+        sumFringe = (psImage*)psBinaryOp(sumFringe, sumFringe, "+", fringeImages->data[i]);
+    }
+    psFree(scale);
+
+    // subtract the resulting fringe frame
+    readout->image = (psImage*)psBinaryOp(readout->image, readout->image, "-", sumFringe);
+
+    return sumFringe;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmFringeStats.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmFringeStats.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmFringeStats.h	(revision 15358)
@@ -0,0 +1,214 @@
+/* @file pmFringeStats.h
+ * @brief Measure fringe statistics, and apply correction
+ *
+ * @author Eugene Magnier, IfA
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:15 $
+ * Copyright 2004-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_FRINGE_STATS
+#define PM_FRINGE_STATS
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// pmFringeRegions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/// Fringe measurement regions.
+///
+/// Fringes are measured within a box of size dX,dY.  A large scale smoothing is performed by subtracting the
+/// background within large divisions of the image.  The coordinates of the fringe points and the mask may be
+/// NULL, which means that they will be generated when required.
+typedef struct
+{
+    int nRequested;                     // Number of fringe points selected
+    int nAccepted;                      // Number of fringe points not masked
+    int dX;                             // Median box half-width
+    int dY;                             // Median box half-height
+    int nX;                             // Number of large-scale smoothing divisions in x (col)
+    int nY;                             // Number of large-scale smoothing divisions in y (row)
+    psVector *x;                        // Fringe point coordinates (col), or NULL
+    psVector *y;                        // Fringe point coordinates (row), or NULL
+    psVector *mask;                     // Fringe point on/off mask, or NULL
+}
+pmFringeRegions;
+
+/// Allocate fringe regions
+pmFringeRegions *pmFringeRegionsAlloc (int nPts, ///< Number of fringe points to create
+                                       int dX, ///< Half-width of fringe boxes
+                                       int dY, ///< Half-height of fringe boxes
+                                       int nX, ///< Smoothing scale in x
+                                       int nY ///< Smoothing scale in y
+                                      );
+
+/// Generate the fringe points
+///
+/// Fringe points are generated randomly over the image.  No effort is made to avoid masked regions (indeed,
+/// the function knows nothing about masks).  If the random number generator is NULL, then a new one will be
+/// used.
+bool pmFringeRegionsCreatePoints(pmFringeRegions *fringe, ///< Fringe regions to generate
+                                 const psImage *image, ///< Image for the regions (defines the size)
+                                 psRandom *random ///< Random number generator, or NULL
+                                );
+
+/// Write the regions to a FITS file
+///
+/// The fringe regions are written to the FITS file, with the given extension name.  The header is
+/// supplemented with scalar values dX, dY, nX and nY (as PSFRNGDX, PSFRNGDY, PSFRNGNX, PSFRNGNY) from the
+/// fringe regions, while the fringe coordinates and mask are written as a FITS table (as x, y, mask).
+bool pmFringeRegionsWriteFits(psFits *fits, ///< Output FITS file
+                              psMetadata *header, ///< Additional headers to write, or NULL
+                              const pmFringeRegions *regions, ///< Regions to write
+                              const char *extname ///< Extension name, or NULL
+                             );
+
+/// Read the regions from a FITS file
+///
+/// The fringe regions are read from the FITS file, at the given extension name.  The scalars are retrieved
+/// from the header, while the table provides the fringe coordinates and mask.
+pmFringeRegions *pmFringeRegionsReadFits(psMetadata *header, ///< Header to read, or NULL
+        const psFits *fits, ///< Input FITS file
+        const char *extname ///< Extension name, or NULL
+                                        );
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// pmFringeStats
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/// Fringe measurements for a particular image
+///
+/// Measurements of the median and stdev are made at each of the fringe regions.
+typedef struct
+{
+    pmFringeRegions *regions;           ///< Fringe regions
+    psVector *f;                        ///< Fringe point median
+    psVector *df;                       ///< Fringe point stdev
+}
+pmFringeStats;
+
+/// Allocate fringe statistics
+pmFringeStats *pmFringeStatsAlloc(pmFringeRegions *regions // The fringe regions which will be measured
+                                 );
+
+/// Measure the fringe statistics for an image
+///
+/// Given an input image and fringe regions at which to measure, measures the median and stdev at each of the
+/// fringe points.  If the fringe points are undefined, they are generated.
+pmFringeStats *pmFringeStatsMeasure(pmFringeRegions *fringe, ///< Fringe regions at which to measure
+                                    const pmReadout *readout, ///< Readout for which to measure
+                                    psMaskType maskVal ///< Mask value for image
+                                   );
+
+/// Write the fringe stats for an image to a FITS table
+///
+/// The fringe measurements are written to the FITS file with the given extension name.  The median and stdev
+/// measurements are written as a FITS table (as f and df).
+bool pmFringeStatsWriteFits(psFits *fits, ///< FITS file to which to write
+                            psMetadata *header, ///< Additional headers to write, or NULL
+                            const pmFringeStats *fringe, ///< Fringe statistics to be written
+                            const char *extname ///< Extension name for table
+                           );
+
+/// Read the fringe stats for an image from a FITS table
+///
+/// The fringe measurements are read from the FITS file, at the given extension name.  The table provides the
+/// median and stdev measurements.  It is assumed that the fringe measurements correspond to the regions
+/// provided.
+pmFringeStats *pmFringeStatsReadFits(psMetadata *header, ///< Header to read, or NULL
+                                     const psFits *fits, ///< FITS file from which to read
+                                     const char *extname, ///< Extension name to read
+                                     pmFringeRegions *regions ///< Corresponding regions
+                                    );
+
+/// Concatenate the fringe stats for several readouts into a single fringe stats.
+///
+/// Each readout of each chip must be measured separately (so as to avoid any gaps between the cells, as in
+/// the case for GPC).  But the fit must be performed with all the readouts belonging to a chip (in order to
+/// get a secure measurement of the fringe amplitudes).  To do so, we need to concatenate the fringe
+/// measurements for each of the chip components.  This function generates a new pmFringeStats from
+/// concatenating those in the array.  The corresponding pmFringeRegions is also generated.
+pmFringeStats *pmFringeStatsConcatenate(const psArray *fringes, ///< Array of pmFringeStats for the readouts
+                                        const psVector *x0, ///< Offset in x for the readout
+                                        const psVector *y0 ///< Offset in y for the readout
+                                       );
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Input/output for multiple pmFringeStats
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/// Write an array of fringes measurements to a FITS table.
+///
+/// Writes an array of fringe measurements for a cell as a FITS table in the analysis metadata.  The array of
+/// fringe statistics must all use the same fringe regions (or there is no point in storing them all
+/// together).  The header is supplemented with scalar values dX, dY, nX and nY (as PSFRNGDX, PSFRNGDY,
+/// PSFRNGNX, PSFRNGNY) from the fringe regions, while the fringe coordinates and mask are written as a FITS
+/// table (as x, y, mask, f, df; f and df are vectors).
+bool pmFringesFormat(pmCell *cell,   ///< Cell for which to write
+                     psMetadata *header, ///< Header, or NULL
+                     const psArray *fringes ///< Array of pmFringeStats, all for the same pmFringeRegion
+                    );
+
+/// Parses an array of fringes measurements from a FITS table.
+///
+/// The fringes for the cell are read from the FITS table in the analysis metadata.  The table provides the
+/// region and the (possibly multiple) fringe statistics for that region.  The current extension is used if
+/// the extension name is not provided.
+psArray *pmFringesParse(pmCell *cell ///< Cell for which to read fringes
+                       );
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// pmFringeScale
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/// The fringe correction solution
+typedef struct
+{
+    int nFringeFrames;                  ///< Number of fringe frames
+    psVector *coeff;                    ///< Fringe coefficients; size = nFringeFrames
+    psVector *coeffErr;                 ///< Error in fringe coefficients; size = nFringeFrames
+}
+pmFringeScale;
+
+/// Measure the scales for the fringe correction
+///
+/// Given a fringe measurement for a science image, and an array of template fringe measurements, this
+/// function measures the contribution of each of the templates to the input.  Rejection is performed on the
+/// fringe regions, to weed out stars etc.
+pmFringeScale *pmFringeScaleMeasure(pmFringeStats *science, ///< Fringe measurements from science image
+                                    psArray *fringes, ///< Array of fringe measurements from templates
+                                    float rej, ///< Rejection threshold (in standard deviations)
+                                    unsigned int nIter, ///< Maximum number of iterations
+                                    float keepFrac ///< Minimum fraction of regions to keep
+                                   );
+
+/// Allocate fringe scales
+pmFringeScale *pmFringeScaleAlloc(int nFringeFrames ///< Number of fringe frames
+                                 );
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Fringe correction
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/// Solve for and apply the fringe correction
+///
+/// This is a wrapper around each of the fringe correction components to measure the fringe points, solve for
+/// the fringe correction, and apply the fringe correction.  The input fringe images are modified (scaled by
+/// the solution coefficients in order to correct the science image).  Returns the summed fringe image.
+psImage *pmFringeCorrect(pmReadout *in, ///< Input science image
+                         pmFringeRegions *fringes, ///< The fringe regions used
+                         psArray *fringeImages, ///< Fringe template images to use in correction
+                         psArray *fringeStats, ///< Fringe stats (for templates) to use in correction
+                         psMaskType maskVal, ///< Value to mask for science image
+                         float rej,     ///< Rejection threshold, for pmFringeScaleMeasure
+                         unsigned int nIter, ///< Maximum number of iterations, for pmFringeScaleMeasure
+                         float keepFrac ///< Minimum fraction of regions to keep, for pmFringeScaleMeasure
+                        );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmMaskBadPixels.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmMaskBadPixels.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmMaskBadPixels.c	(revision 15358)
@@ -0,0 +1,223 @@
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+#include "pmFPAMaskWeight.h"
+#include "pmMaskBadPixels.h"
+
+bool pmMaskBadPixels(pmReadout *input, const pmReadout *mask, psMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(input, false);
+    PS_ASSERT_PTR_NON_NULL(input->mask, false);
+    PS_ASSERT_IMAGE_TYPE(input->mask, PS_TYPE_MASK, false);
+
+    PS_ASSERT_PTR_NON_NULL(mask, false);
+    PS_ASSERT_PTR_NON_NULL(mask->mask, false);
+    PS_ASSERT_IMAGE_TYPE(mask->mask, PS_TYPE_MASK, false);
+
+    psImage *inMask = input->mask;
+    psImage *exMask = mask->mask;
+
+    // Add mask MD5 to header
+    pmHDU *hdu = pmHDUFromReadout(input);  // HDU of interest
+    psVector *md5 = psImageMD5(mask->mask); // md5 hash
+    psString md5string = psMD5toString(md5); // String
+    psFree(md5);
+    psStringPrepend(&md5string, "MASK image MD5: ");
+    psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+                     md5string, "");
+    psFree(md5string);
+
+    int rowMax = input->row0 + inMask->numRows;
+    int colMax = input->col0 + inMask->numCols;
+
+    if (mask->row0 > input->row0 || mask->col0 > input->col0 ||
+            mask->row0 + exMask->numRows < rowMax || mask->col0 + exMask->numCols < colMax) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                "Input image size exceeds that of mask image: (%d, %d) vs (%d, %d)",
+                inMask->numRows, inMask->numCols, exMask->numRows, exMask->numCols);
+        return false;
+    }
+
+    // Determine total offset based on image offset with chip offset
+    // XXX if we choose to correct for the readout location, apply input->col0,row0 here
+    int offCol = input->col0 - mask->col0;
+    int offRow = input->row0 - mask->row0;
+
+    // masks are both of type PS_TYPE_MASK
+    psMaskType **exVal = exMask->data.U8;
+    psMaskType **inVal = inMask->data.U8;
+
+    // apply exMask values
+    if (maskVal) {
+        // set raised pixels in exMask which are selected by maskVal
+        for (int j = 0; j < inMask->numRows; j++) {
+            int xJ = j - offRow;
+            for (int i = 0; i < inMask->numCols; i++) {
+                int xI = i - offCol;
+                inVal[j][i] |= (maskVal & exVal[xJ][xI]);
+            }
+        }
+    }
+
+    psTime *time = psTimeGetNow(PS_TIME_TAI); // The time now, used for reporting
+    psString timeString = psTimeToISO(time); // String with time
+    psFree(time);
+    psStringPrepend(&timeString, "Static mask (selecting %x) applied at ", maskVal);
+    psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+                     timeString, "");
+    psFree(timeString);
+
+    return true;
+}
+
+
+psImage *pmMaskFlagSuspectPixels(psImage *out, const pmReadout *readout, float rej,
+                                 psMaskType maskVal, float frac, psRandom *rng)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, NULL);
+    PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(frac, 0.0, 1.0, NULL);
+    PS_ASSERT_IMAGE_NON_NULL(readout->image, NULL);
+    PS_ASSERT_IMAGE_NON_EMPTY(readout->image, NULL);
+    PS_ASSERT_IMAGE_TYPE(readout->image, PS_TYPE_F32, NULL);
+    if (readout->mask) {
+        PS_ASSERT_IMAGE_NON_EMPTY(readout->mask, NULL);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(readout->image, readout->mask, NULL);
+        PS_ASSERT_IMAGE_TYPE(readout->mask, PS_TYPE_MASK, NULL);
+    }
+    if (out) {
+        PS_ASSERT_IMAGE_NON_EMPTY(out, NULL);
+        PS_ASSERT_IMAGE_TYPE(out, PS_TYPE_S32, NULL);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(readout->image, out, NULL);
+    }
+
+    psImage *image = readout->image;    // Image of interest
+    psImage *mask = readout->mask;      // Corresponding mask
+
+    if (rng) {
+        psMemIncrRefCounter(rng);
+    } else {
+        rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+    }
+
+    // XXX note that this now will accept any of several stats options
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    stats->nSubsample = frac * image->numCols * image->numRows;
+    if (!psImageBackground(stats, NULL, image, mask, maskVal, rng) ||
+            !isfinite(stats->robustMedian) || !isfinite(stats->robustUQ) || !isfinite(stats->robustLQ)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to measure image statistics.\n");
+        psFree(stats);
+        psFree(rng);
+        return NULL;
+    }
+    psFree(rng);
+
+    float median = stats->robustMedian; // Median value
+    float stdev = stats->robustStdev; // Estimate of the standard deviation
+    psFree(stats);
+
+    if (!out) {
+        out = psImageAlloc(image->numCols, image->numRows, PS_TYPE_S32);
+        psImageInit(out, 0);
+    }
+
+    for (int y = 0; y < image->numRows; y++) {
+        for (int x = 0; x < image->numCols; x++) {
+            if (fabs((image->data.F32[y][x] - median) / stdev) >= rej &&
+                    (!mask || !(mask->data.PS_TYPE_MASK_DATA[y][x] & maskVal))) {
+                out->data.S32[y][x]++;
+            }
+        }
+    }
+
+    return out;
+}
+
+
+psImage *pmMaskIdentifyBadPixels(const psImage *suspects, float thresh, psMaskType maskVal)
+{
+    PS_ASSERT_IMAGE_NON_NULL(suspects, NULL);
+    PS_ASSERT_IMAGE_NON_EMPTY(suspects, NULL);
+    PS_ASSERT_IMAGE_TYPE(suspects, PS_TYPE_S32, NULL);
+
+    float limit = NAN;                  // Limit for masking
+    if (thresh > 0) {
+        psStats *stats = psStatsAlloc(PS_STAT_CLIPPED_STDEV); // Statistics
+        stats->clipSigma = 5.0;
+        stats->clipIter = 1;
+        if (!psImageStats(stats, suspects, NULL, 0) || !isfinite(stats->clippedStdev)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to perform statistics.\n");
+            psFree(stats);
+            return NULL;
+        }
+        limit = thresh * stats->clippedStdev;
+        psFree(stats);
+    } else if (thresh < 0) {
+        psStats *stats = psStatsAlloc(PS_STAT_MAX); // Statistics
+        if (!psImageStats(stats, suspects, NULL, 0)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to perform statistics.\n");
+            psFree(stats);
+            return NULL;
+        }
+        psHistogram *histo = psHistogramAlloc(-0.5, stats->max + 0.5, stats->max + 1);
+        psFree(stats);
+        if (!psImageHistogram(histo, suspects, NULL, 0)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to generate histogram.\n");
+            psFree(histo);
+            return NULL;
+        }
+
+        // Find the mode.  Since this is a Poisson distribution (more or less), this should also be the mean
+        // and variance.
+        int max = 0;                    // Index of the mode
+        for (int i = 0; i < histo->nums->n; i++) {
+            if (histo->nums->data.F32[i] > histo->nums->data.F32[max]) {
+                max = i;
+            }
+        }
+
+        // Since the mode is most likely zero, we add one to get something realistic.  Then "thresh" is
+        // negative, so we subtract instead of add.
+        limit = max + 1.0 - thresh * sqrtf((float)max + 1.0);
+
+    } else {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Threshold (%f) must be a positive or negative real number.\n", thresh);
+        return NULL;
+    }
+
+    psImage *badpix = psImageAlloc(suspects->numCols, suspects->numRows, PS_TYPE_MASK); // Bad pixel mask
+    psImageInit(badpix, 0);
+
+    if (psTraceGetLevel("psModules.detrend") > 9) {
+        psStats *stats = psStatsAlloc(PS_STAT_MIN | PS_STAT_MAX); // Statistics
+        psImageStats(stats, suspects, NULL, 0);
+        psHistogram *histo = psHistogramAlloc(-0.5, stats->max + 0.5, stats->max + 1);
+        psImageHistogram(histo, suspects, NULL, 0);
+        for (int i = 0; i < histo->nums->n; i++) {
+            printf("%f --> %f : %f\n", histo->bounds->data.F32[i], histo->bounds->data.F32[i + 1],
+                   histo->nums->data.F32[i]);
+        }
+        psFree(stats);
+        psFree(histo);
+        printf("Threshold: %f\n", limit);
+    }
+
+    for (int y = 0; y < suspects->numRows; y++) {
+        for (int x = 0; x < suspects->numCols; x++) {
+            if (suspects->data.S32[y][x] >= limit) {
+                badpix->data.PS_TYPE_MASK_DATA[y][x] = maskVal;
+            }
+        }
+    }
+
+    return badpix;
+}
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmMaskBadPixels.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmMaskBadPixels.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmMaskBadPixels.h	(revision 15358)
@@ -0,0 +1,55 @@
+/* @file pmMaskBadPixels.h
+ * @brief Mask bad pixels
+ *
+ * @author Ross Harman, MHPCC
+ * @author Eugene Magnier, IfA
+ *
+ * @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2004 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_MASK_BAD_PIXELS_H
+#define PM_MASK_BAD_PIXELS_H
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+/// Applies the bad pixel mask to the input
+///
+/// Pixels marked as bad within the mask are marked as bad within the input image's mask.  If maskVal is
+/// non-zero, all pixels in the mask have any of the same bits sets as maskVal shall have the corresponding
+/// bits raised.  If maskVal is zero, any zero pixels in the mask are OR-ed with PM_MASK_BAD.  Position
+/// offsets (such as due to trimming) between the input and mask are applied so that the same pixels are
+/// referred to.  The science readout must already have a supplied mask element (use eg. pmReadoutSetMask).
+/// The supplied mask image must be of MASK type
+bool pmMaskBadPixels(pmReadout *input,  ///< Input science image
+                     const pmReadout *mask, ///< Mask image to apply
+                     psMaskType maskVal ///< Mask value to apply
+                    );
+
+/// Find pixels outlying from the background, flagging suspect pixels
+///
+/// Pixels more than "rej" standard deviations from the background level (in flat-fielded,
+/// background-subtracted images) have the corresponding pixel in the "suspect pixels" image incremented.
+/// After accumulating over a suitable sample of images, bad pixels should have a high value in the suspect
+/// pixels image, allowing them to be identified.  The suspect pixels image is of type S32.
+psImage *pmMaskFlagSuspectPixels(psImage *out, ///< Suspected bad pixels image, or NULL
+                                 const pmReadout *readout, ///< Readout to inspect
+                                 float rej, ///< Rejection threshold (standard deviations)
+                                 psMaskType maskVal, ///< Mask value for statistics
+                                 float frac, ///< Fraction of pixels to consider
+                                 psRandom *rng ///< Random number generator
+                                );
+
+/// Identify bad pixels from the suspect pixels image
+///
+/// Bad pixels are identified from the suspect pixels image (accumulated over a large number of images).
+/// Pixels marked as suspect in more than "thresh" standard deviations from the mean are identified as bad
+/// pixels (output image).  If "thresh" is negative, a Poisson is assumed.
+psImage *pmMaskIdentifyBadPixels(const psImage *suspects, ///< Accumulated suspect pixels image
+                                 float thresh, ///< Threshold for bad pixel (standard deviations)
+                                 psMaskType maskVal ///< Value to set for bad pixels
+                                );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmNonLinear.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmNonLinear.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmNonLinear.c	(revision 15358)
@@ -0,0 +1,93 @@
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmNonLinear.h"
+
+pmReadout *pmNonLinearityPolynomial(pmReadout *inputReadout, const psPolynomial1D *input1DPoly)
+{
+    PS_ASSERT_PTR_NON_NULL(inputReadout, NULL);
+    PS_ASSERT_PTR_NON_NULL(inputReadout->image, NULL);
+    PS_ASSERT_IMAGE_TYPE(inputReadout->image, PS_TYPE_F32, NULL);
+    PS_ASSERT_PTR_NON_NULL(input1DPoly, NULL);
+
+    psImage *image = inputReadout->image; // Image to correct
+    for (int i = 0; i < image->numRows; i++) {
+        for (int j = 0; j < image->numCols; j++) {
+            image->data.F32[i][j] = psPolynomial1DEval(input1DPoly, image->data.F32[i][j]);
+        }
+    }
+    return inputReadout;
+}
+
+// set the bin closest to the corresponding value.  
+#define PS_BIN_FOR_VALUE(RESULT, VECTOR, VALUE) { \
+       	psVectorBinaryDisectResult result; \
+       	psScalar tmpScalar; \
+       	tmpScalar.type.type = PS_TYPE_F32; \
+	tmpScalar.data.F32 = (VALUE); \
+	RESULT = psVectorBinaryDisect (&result, VECTOR, &tmpScalar); \
+	switch (result) { \
+	  case PS_BINARY_DISECT_PASS: \
+            break; \
+	  case PS_BINARY_DISECT_OUTSIDE_RANGE: \
+            numPixels ++; \
+	    break; \
+	  case PS_BINARY_DISECT_INVALID_INPUT: \
+	  case PS_BINARY_DISECT_INVALID_TYPE: \
+	    psAbort ("programming error"); \
+	    break; \
+        } }
+
+pmReadout *pmNonLinearityLookup(pmReadout *inputReadout, const psVector *inFlux, const psVector *outFlux)
+{
+    PS_ASSERT_PTR_NON_NULL(inputReadout, NULL);
+    PS_ASSERT_PTR_NON_NULL(inputReadout->image, NULL);
+    PS_ASSERT_IMAGE_TYPE(inputReadout->image, PS_TYPE_F32, NULL);
+    PS_ASSERT_PTR_NON_NULL(inFlux, NULL);
+    if (inFlux->n < 2) {
+        psError(PS_ERR_UNKNOWN, true,
+                "pmNonLinearityLookup(): input vector less than 2 elements.  Returning inputReadout image.");
+        return(inputReadout);
+    }
+    PS_ASSERT_PTR_NON_NULL(outFlux,NULL);
+    psS32 tableSize = inFlux->n;
+    if (inFlux->n != outFlux->n) {
+        tableSize = PS_MIN(inFlux->n, outFlux->n);
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: pmNonLinear.c: pmNonLinearityLookup(): "
+                 "input vectors have different sizes (%ld, %ld)\n",
+                 inFlux->n, outFlux->n);
+    }
+    PS_ASSERT_VECTOR_TYPE(inFlux, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_TYPE(outFlux, PS_TYPE_F32, NULL);
+
+    psImage *image = inputReadout->image; // Input image
+    int numPixels = 0;                  // Number of pixels outside the range
+    int binNum;
+
+    for (int i = 0; i < image->numRows; i++) {
+        for (int j = 0; j < image->numCols; j++) {
+	    float value = image->data.F32[i][j];
+            PS_BIN_FOR_VALUE(binNum, inFlux, value);
+
+	    // Perform linear interpolation.
+	    // XXX this will result in non-sensical results if inFlux contains equal-value
+	    // bins.  either enforce d(inFlux)/d(binNum) > 0 or see psStats.c PS_BIN_INTERPOLATE
+	    float slope = 
+		(outFlux->data.F32[binNum + 1] - outFlux->data.F32[binNum]) /
+		(inFlux->data.F32[binNum + 1] - inFlux->data.F32[binNum]);
+	    image->data.F32[i][j] = slope*(value - inFlux->data.F32[binNum]) + outFlux->data.F32[binNum];
+        }
+    }
+    if (numPixels > 0) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: pmNonLinear.c: pmNonLinearityLookup(): %d pixels outside table.", numPixels);
+    }
+    return inputReadout;
+}
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmNonLinear.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmNonLinear.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmNonLinear.h	(revision 15358)
@@ -0,0 +1,35 @@
+/* @file pmNonLinear.h
+ * @brief Perform non-linear correction through polynomial or table lookup
+ *
+ * @author George Gusciora, MHPCC
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2004 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_NON_LINEAR_H
+#define PM_NON_LINEAR_H
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+/// Correct non-linearity through polynomial
+///
+/// Applies a polynomial to the flux of each pixel in the input image to determine the corrected flux.
+pmReadout *pmNonLinearityPolynomial(pmReadout *in, ///< Input image, to correct
+                                    const psPolynomial1D *coeff ///< Polynomial for non-linearity correction
+                                   );
+
+/// Correct non-linearity through table lookup
+///
+/// For each pixel in the input image, performs linear interpolation on the table (from the two vectors) to
+/// determine the corrected flux.
+pmReadout *pmNonLinearityLookup(pmReadout *in, ///< Input image, to correct
+                                const psVector *inFlux, ///< Table column with input fluxes
+                                const psVector *outFlux ///< Table column with output fluxes
+                               );
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmOverscan.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmOverscan.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmOverscan.c	(revision 15358)
@@ -0,0 +1,401 @@
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPACalibration.h"
+
+#include "pmOverscan.h"
+
+#define SMOOTH_NSIGMA 4.0               // Number of Gaussian sigma the smoothing kernel extends
+
+static void pmOverscanOptionsFree(pmOverscanOptions *options)
+{
+    psFree(options->stat);
+    psFree(options->poly);
+    psFree(options->spline);
+}
+
+pmOverscanOptions *pmOverscanOptionsAlloc(bool single, pmFit fitType, unsigned int order, psStats *stat,
+                                          int boxcar, float gauss)
+{
+    pmOverscanOptions *opts = psAlloc(sizeof(pmOverscanOptions));
+    psMemSetDeallocator(opts, (psFreeFunc)pmOverscanOptionsFree);
+
+    // Inputs
+    opts->single = single;
+    opts->constant = false;
+    opts->fitType = fitType;
+    opts->order = order;
+    opts->stat = psMemIncrRefCounter(stat);
+
+    // Smoothing
+    opts->boxcar = boxcar;
+    opts->gauss = gauss;
+
+    // Outputs
+    opts->poly = NULL;
+    opts->spline = NULL;
+
+    return opts;
+}
+
+// Produce an overscan vector from an array of pixels
+psVector *pmOverscanVector(float *chi2, // chi^2 from fit
+			   pmOverscanOptions *overscanOpts, // Overscan options
+			   const psArray *pixels, // Array of vectors containing the pixel values
+			   psStats *myStats // Statistic to use in reducing the overscan
+    )
+{
+    assert(overscanOpts);
+    assert(pixels);
+    assert(myStats);
+
+    psStatsOptions statistic = psStatsSingleOption(myStats->options); // Statistic to use
+    assert(statistic != 0);
+
+    // Reduce the overscans
+    psVector *reduced = psVectorAlloc(pixels->n, PS_TYPE_F32); // Overscan for each row
+    psVector *ordinate = psVectorAlloc(pixels->n, PS_TYPE_F32); // Ordinate
+    psVector *mask = psVectorAlloc(pixels->n, PS_TYPE_U8); // Mask for fitting
+
+    for (int i = 0; i < pixels->n; i++) {
+        psVector *values = pixels->data[i]; // Vector with overscan values
+        if (values->n > 0) {
+            mask->data.U8[i] = 0;
+            ordinate->data.F32[i] = 2.0*(float)i/(float)pixels->n - 1.0; // Scale to [-1,1]
+            psVectorStats(myStats, values, NULL, NULL, 0);
+            reduced->data.F32[i] = psStatsGetValue(myStats, statistic);
+        } else if (overscanOpts->fitType == PM_FIT_NONE) {
+            psError(PS_ERR_UNKNOWN, true, "The overscan is not supplied for all points on the "
+                    "image, and no fit is requested.\n");
+            return NULL;
+        } else {
+            // We'll fit this one out
+            mask->data.U8[i] = 1;
+        }
+    }
+
+    // Smooth the reduced vector
+    if (overscanOpts->boxcar > 0) {
+        psVector *smoothed = psVectorBoxcar(NULL, reduced, overscanOpts->boxcar); // Smoothed vector
+        psFree(reduced);
+        reduced = smoothed;
+    }
+    if (isfinite(overscanOpts->gauss) && overscanOpts->gauss > 0) {
+        if (overscanOpts->boxcar > 0) {
+            psWarning("Gaussian smoothing the boxcar smoothed overscan --- you asked for it.");
+        }
+        psVector *smoothed = psVectorSmooth(NULL, reduced, overscanOpts->gauss, SMOOTH_NSIGMA);
+        psFree(reduced);
+        reduced = smoothed;
+    }
+
+    // Fit the overscan, if required
+    psVector *fitted;                   // Fitted overscan values
+    switch (overscanOpts->fitType) {
+      case PM_FIT_NONE:
+        // No fitting --- that's easy.
+        fitted = psMemIncrRefCounter(reduced);
+        break;
+      case PM_FIT_POLY_ORD:
+        if (overscanOpts->poly && (overscanOpts->poly->nX != overscanOpts->order ||
+                                   overscanOpts->poly->type != PS_POLYNOMIAL_ORD)) {
+            psFree(overscanOpts->poly);
+            overscanOpts->poly = NULL;
+        }
+        if (! overscanOpts->poly) {
+            overscanOpts->poly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, overscanOpts->order);
+        }
+        psVectorFitPolynomial1D(overscanOpts->poly, mask, 1, reduced, NULL, ordinate);
+        fitted = psPolynomial1DEvalVector(overscanOpts->poly, ordinate);
+        break;
+      case PM_FIT_POLY_CHEBY:
+        if (overscanOpts->poly && (overscanOpts->poly->nX != overscanOpts->order ||
+                                   overscanOpts->poly->type != PS_POLYNOMIAL_CHEB)) {
+            psFree(overscanOpts->poly);
+            overscanOpts->poly = NULL;
+        }
+        if (! overscanOpts->poly) {
+            overscanOpts->poly = psPolynomial1DAlloc(PS_POLYNOMIAL_CHEB, overscanOpts->order);
+        }
+        psVectorFitPolynomial1D(overscanOpts->poly, mask, 1, reduced, NULL, ordinate);
+        fitted = psPolynomial1DEvalVector(overscanOpts->poly, ordinate);
+        break;
+      case PM_FIT_SPLINE:
+        // XXX I don't think psSpline1D is up to scratch yet --- it has no mask, and requires an
+        // input spline
+        overscanOpts->spline = psVectorFitSpline1D(reduced, ordinate);
+        fitted = psSpline1DEvalVector(overscanOpts->spline, ordinate);
+        break;
+      default:
+        psError(PS_ERR_UNKNOWN, true, "Unknown value for the fitting type: %d\n", overscanOpts->fitType);
+        return NULL;
+        break;
+    }
+
+    if (chi2) {
+        *chi2 = 0.0;                    // chi^2 (sort of)
+        for (int i = 0; i < reduced->n; i++) {
+            *chi2 += PS_SQR(fitted->data.F32[i] - reduced->data.F32[i]);
+        }
+    }
+
+    psFree(reduced);
+    psFree(ordinate);
+    psFree(mask);
+
+    return fitted;
+}
+
+bool pmOverscanUpdateHeader (pmHDU *hdu, pmOverscanOptions *overscanOpts, float chi2) {
+
+    psString comment = NULL;    // Comment to add
+
+    switch (overscanOpts->fitType) {
+      case PM_FIT_POLY_ORD:
+      case PM_FIT_POLY_CHEBY: {
+	  psStringAppend(&comment, "Overscan fit (chi2: %.2f): ", chi2);
+	  psPolynomial1D *poly = overscanOpts->poly; // The polynomial
+	  for (int i = 0; i < poly->nX; i++) {
+	      psStringAppend(&comment, "%.1f ", poly->coeff[i]);
+	  }
+	  psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, comment, "");
+	  psFree(comment);
+	  comment = NULL;
+
+	  // write metadata header value
+	  psMetadataAddF32(hdu->header, PS_LIST_TAIL, "OVER_VAL", PS_META_REPLACE,
+			   "Overscan value", poly->coeff[0]);
+	  psMetadataAddF32(hdu->header, PS_LIST_TAIL, "OVER_SIG", PS_META_REPLACE,
+			   "Overscan stdev", poly->coeffErr[0]);
+	  break;
+      }
+      case PM_FIT_SPLINE: {
+	  psSpline1D *spline = overscanOpts->spline; // The spline
+	  for (int i = 0; i < spline->n; i++) {
+	      psStringAppend(&comment, "Overscan fit (chi2: %.2f) %d:", chi2, i);
+	      psPolynomial1D *poly = spline->spline[i]; // i-th polynomial
+	      for (int j = 0; j < poly->nX; j++) {
+		  psStringAppend(&comment, "%.1f ", poly->coeff[i]);
+	      }
+	      psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+			       comment, "");
+	      psFree(comment);
+	      comment = NULL;
+	  }
+	  // write metadata header value
+	  psMetadataAddF32(hdu->header, PS_LIST_TAIL, "OVER_VAL", PS_META_REPLACE,
+			   "Overscan value", NAN);
+	  psMetadataAddF32(hdu->header, PS_LIST_TAIL, "OVER_SIG", PS_META_REPLACE,
+			   "Overscan stdev", NAN);
+	  break;
+      }
+      case PM_FIT_NONE:
+	break;
+      default:
+	psAbort("Should never get here!!!\n");
+    }
+    return true;
+}
+
+bool pmOverscanSubtract (pmReadout *input, pmOverscanOptions *overscanOpts) {
+
+    assert (input);
+
+    if (overscanOpts == NULL) return true; // no overscan subtraction requested
+
+    pmHDU *hdu = pmHDUFromReadout(input);  // HDU of interest
+    psImage *image = input->image;
+
+    // check for 'soft bias' (simple, fixed offset to be subtracted)
+    if (overscanOpts->constant) {
+	// write metadata header value
+	psMetadataAddF32(hdu->header, PS_LIST_TAIL, "OVER_VAL", PS_META_REPLACE, "Overscan value",
+			 overscanOpts->value);
+	psMetadataAddF32(hdu->header, PS_LIST_TAIL, "OVER_SIG", PS_META_REPLACE, "Overscan stdev", NAN);
+
+	(void)psBinaryOp(input->image, input->image, "-", psScalarAlloc((float)overscanOpts->value, PS_TYPE_F32));
+
+	return true;
+    }
+
+    // we are performing a statitical analysis of the overscan region
+
+    // Check for an unallowable pmFit.
+    if (overscanOpts->fitType != PM_FIT_NONE && overscanOpts->fitType != PM_FIT_POLY_ORD &&
+	overscanOpts->fitType != PM_FIT_POLY_CHEBY && overscanOpts->fitType != PM_FIT_SPLINE) {
+	psError(PS_ERR_UNKNOWN, true, "Invalid fit type (%d).  Returning original image.\n",
+		overscanOpts->fitType);
+	return false;
+    }
+
+    psList *overscans = input->bias; // List of the overscan images
+
+    psStatsOptions statistic = psStatsSingleOption(overscanOpts->stat->options); // Statistic to use
+    if (statistic == 0) {
+	psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Multiple or no statistics options set: %p\n",
+		overscanOpts->stat);
+	return false;
+    }
+    psStats *stats = psStatsAlloc(statistic); // A new psStats, to avoid clobbering original
+
+    psString comment = NULL;    // Comment to add
+    psStringAppend(&comment, "Subtracting overscan (stat %x; type %x; order %d)",
+		   statistic, overscanOpts->fitType, overscanOpts->order);
+    psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+		     comment, "");
+    psFree(comment);
+
+    // Reduce all overscan pixels to a single value
+    if (overscanOpts->single) {
+	psVector *pixels = psVectorAlloc(0, PS_TYPE_F32);
+	psListIterator *iter = psListIteratorAlloc(overscans, PS_LIST_HEAD, false); // Iterator
+	psImage *overscan = NULL;   // Overscan image from iterator
+	while ((overscan = psListGetAndIncrement(iter))) {
+	    int index = pixels->n;  // Index
+	    pixels = psVectorRealloc(pixels, pixels->n + overscan->numRows * overscan->numCols);
+	    pixels->n += overscan->numRows * overscan->numCols;
+	    for (int i = 0; i < overscan->numRows; i++) {
+		memcpy(&pixels->data.F32[index], overscan->data.F32[i],
+		       overscan->numCols * sizeof(psF32));
+		index += overscan->numCols;
+	    }
+	}
+	psFree(iter);
+
+	(void)psVectorStats(stats, pixels, NULL, NULL, 0);
+	psFree(pixels);
+	double reduced = psStatsGetValue(stats, statistic); // Result of statistics
+
+	psString comment = NULL;    // Comment to add
+	psStringAppend(&comment, "Overscan value: %f", reduced);
+	psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, comment, "");
+	psFree(comment);
+
+	// write metadata header value
+	psMetadataAddF32(hdu->header, PS_LIST_TAIL, "OVER_VAL", PS_META_REPLACE, "Overscan value",
+			 reduced);
+	psMetadataAddF32(hdu->header, PS_LIST_TAIL, "OVER_SIG", PS_META_REPLACE, "Overscan stdev", NAN);
+
+	(void)psBinaryOp(image, image, "-", psScalarAlloc((float)reduced, PS_TYPE_F32));
+	psFree(stats);
+	return true;
+    } 
+
+    // We are performing a row-by-row overscan subtraction
+    bool readRows = psMetadataLookupBool(NULL, input->parent->concepts, "CELL.READDIR"); // Read direction
+    float chi2 = NAN;           // chi^2 from fit
+
+    // adjust operation depending on the read direction : need to re-org pixels for columns
+    if (readRows) {
+	// The read direction is rows
+	psArray *pixels = psArrayAlloc(image->numRows); // Array of vectors containing pixels
+	for (int i = 0; i < pixels->n; i++) {
+	    pixels->data[i] = psVectorAlloc(0, PS_TYPE_F32);
+	}
+
+	// Pull the pixels out into the vectors
+	psListIterator *iter = psListIteratorAlloc(overscans, PS_LIST_HEAD, false); // Iterator
+	psImage *overscan = NULL; // Overscan image from iterator
+	while ((overscan = psListGetAndIncrement(iter))) {
+	    // the overscan and image might not be aligned.  pixels->data represents
+	    // the image row pixels.
+	    int diff = overscan->row0 - image->row0; // Offset between the two regions
+	    for (int i = PS_MAX(0,diff); i < PS_MIN(image->numRows, overscan->numRows + diff); i++) {
+		int j = i - diff;
+		// i is row on image
+		// j is row on overscan
+		psVector *values = pixels->data[i];
+		int index = values->n; // Index in the vector
+		values = psVectorRealloc(values, values->n + overscan->numCols);
+		values->n += overscan->numCols;
+		memcpy(&values->data.F32[index], overscan->data.F32[j],
+		       overscan->numCols * PSELEMTYPE_SIZEOF(PS_TYPE_F32));
+		index += overscan->numCols;
+		pixels->data[i] = values; // Update the pointer in case it's moved
+	    }
+	}
+	psFree(iter);
+
+	// Reduce the overscans
+	psVector *reduced = pmOverscanVector(&chi2, overscanOpts, pixels, stats);
+	psFree(pixels);
+	if (! reduced) {
+	    psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to generate overscan vector.\n");
+	    psFree(stats);
+	    return false;
+	}
+
+	// Subtract row by row
+	for (int i = 0; i < image->numRows; i++) {
+	    for (int j = 0; j < image->numCols; j++) {
+		image->data.F32[i][j] -= reduced->data.F32[i];
+	    }
+	}
+	psFree(reduced);
+    } else {
+	// The read direction is columns
+	psArray *pixels = psArrayAlloc(image->numCols); // Array of vectors containing pixels
+	for (int i = 0; i < pixels->n; i++) {
+	    psVector *values = psVectorAlloc(0, PS_TYPE_F32);
+	    pixels->data[i] = values;
+	}
+
+	// Pull the pixels out into the vectors
+	psListIterator *iter = psListIteratorAlloc(overscans, PS_LIST_HEAD, false); // Iterator
+	psImage *overscan = NULL; // Overscan image from iterator
+	while ((overscan = psListGetAndIncrement(iter))) {
+	    // the overscan and image might not be aligned.  pixels->data represents
+	    // the image row pixels.
+	    int diff = overscan->col0 - image->col0; // Offset between the two regions
+	    for (int i = PS_MAX(0,diff); i < PS_MIN(image->numCols, overscan->numCols + diff); i++) {
+		int iFixed = i - diff;
+		// i is column on image
+		// iFixed is column on overscan
+		psVector *values = pixels->data[i];
+		int index = values->n; // Index in the vector
+		values = psVectorRealloc(values, values->n + overscan->numRows);
+		for (int j = 0; j < overscan->numRows; j++) {
+		    values->data.F32[index++] = overscan->data.F32[j][iFixed];
+		}
+		values->n += overscan->numRows;
+		pixels->data[i] = values; // Update the pointer in case it's moved
+	    }
+	}
+	psFree(iter);
+
+	// Reduce the overscans
+	psVector *reduced = pmOverscanVector(&chi2, overscanOpts, pixels, stats);
+	psFree(pixels);
+	if (! reduced) {
+	    psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to generate overscan vector.\n");
+	    psFree(stats);
+	    return false;
+	}
+
+	// Subtract column by column
+	for (int i = 0; i < image->numCols; i++) {
+	    for (int j = 0; j < image->numRows; j++) {
+		image->data.F32[j][i] -= reduced->data.F32[i];
+	    }
+	}
+	psFree(reduced);
+    }
+
+    pmOverscanUpdateHeader (hdu, overscanOpts, chi2);
+    psFree(stats);
+
+    return true;
+
+} // End of overscan subtraction
+    
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmOverscan.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmOverscan.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmOverscan.h	(revision 15358)
@@ -0,0 +1,72 @@
+/* @file pmOverscan.h
+ * @brief Functions to subtract the overscan, used by pmBiasSubtract
+ *
+ * @author George Gusciora, MHPCC
+ * @author Paul Price, IfA
+ * @author Eugene Magnier, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-08-15 20:21:18 $
+ * Copyright 2004--2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_OVERSCAN_H
+#define PM_OVERSCAN_H
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+/// Type of fit to perform
+typedef enum {
+    PM_FIT_NONE,                        ///< No fit
+    PM_FIT_POLY_ORD,                    ///< Fit ordinary polynomial
+    PM_FIT_POLY_CHEBY,                  ///< Fit Chebyshev polynomial
+    PM_FIT_SPLINE                       ///< Fit cubic splines
+} pmFit;
+
+/// Options for overscan subtraction
+///
+/// The overscan subtraction may be performed by reducing all overscan regions to a single value (e.g., if
+/// there is no structure); or the overscan may be fit perpendicular to the read direction (usually the
+/// columns) with a particular functional form; or a single value may be subtracted for each read/scan without
+/// fitting (if the structure defies characterisation).  In any case, statistics are required to reduce
+/// multiple values to a single value (either for the scan, or for the entire overscan regions).
+typedef struct
+{
+    // Inputs
+    bool single;                        ///< Reduce all overscan regions to a single value?
+    bool constant;			///< use a supplied constant value (do not measure region)
+    float value;			///< supplied value if needed (per above)
+    pmFit fitType;                      ///< Type of fit to overscan
+    unsigned int order;                 ///< Order of polynomial, or number of spline pieces
+    psStats *stat;                      ///< Statistic to use when reducing the minor direction
+    int boxcar;                         ///< Boxcar smoothing radius
+    float gauss;                        ///< Gaussian smoothing sigma
+    // Outputs
+    psPolynomial1D *poly;               ///< Result of polynomial fit
+    psSpline1D *spline;                 ///< Result of spline fit
+}
+pmOverscanOptions;
+
+/// Allocator for overscan options
+pmOverscanOptions *pmOverscanOptionsAlloc(bool single, ///< Reduce all overscan regions to a single value?
+                                          pmFit fitType, ///< Type of fit to overscan
+                                          unsigned int order, ///< Order of polynomial, or number of splines
+                                          psStats *stat, ///< Statistic to use
+                                          int boxcar, ///< Boxcar smoothing radius
+                                          float gauss ///< Gaussian smoothing sigma
+                                         );
+
+psVector *pmOverscanVector(float *chi2, // chi^2 from fit
+			   pmOverscanOptions *overscanOpts, // Overscan options
+			   const psArray *pixels, // Array of vectors containing the pixel values
+			   psStats *myStats // Statistic to use in reducing the overscan
+    );
+
+bool pmOverscanUpdateHeader (pmHDU *hdu, pmOverscanOptions *overscanOpts, float chi2);
+
+bool pmOverscanSubtract (pmReadout *input, pmOverscanOptions *overscanOpts);
+
+/// @}
+#endif
+
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmShifts.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmShifts.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmShifts.c	(revision 15358)
@@ -0,0 +1,477 @@
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAUtils.h"
+#include "pmShifts.h"
+
+#define SHIFTS_BUFFER 100               // Buffer size for shifts
+#define TRACE "psModules.detrend"       // Trace facility
+#define FFT_SIZE 25                     // Size at which we use FFT instead of direct convolution
+
+// XXX To do:
+// * Make the table column names configurable, by having a SHIFTS metadata in the camera format, with entries
+//   specifying the column names.
+
+
+static void shiftsFree(pmShifts *shifts)
+{
+    psFree(shifts->x);
+    psFree(shifts->y);
+    psFree(shifts->t);
+    return;
+}
+
+pmShifts *pmShiftsAlloc(bool tRel, bool xyRel)
+{
+    pmShifts *shifts = psAlloc(sizeof(pmShifts));
+    psMemSetDeallocator(shifts, (psFreeFunc)shiftsFree);
+
+    shifts->x = psVectorAllocEmpty(SHIFTS_BUFFER, PS_TYPE_S32);
+    shifts->y = psVectorAllocEmpty(SHIFTS_BUFFER, PS_TYPE_S32);
+    shifts->t = psVectorAllocEmpty(SHIFTS_BUFFER, PS_TYPE_F32);
+    shifts->num = 0;
+
+    // Suitable defaults
+    shifts->tRelative = tRel;
+    shifts->xyRelative = xyRel;
+
+    return shifts;
+}
+
+// Look up the cell within a hash; supplement the hash with a new value if it doesn't exist
+static pmShifts *cellVectors(psHash *shifts, // Hash of shifts
+                             const char *cellName, // Key for hash
+                             bool tRel, bool xyRel // Are the shifts relative?
+    )
+{
+    assert(shifts);
+    assert(cellName);
+
+    // Find the appropriate cell
+    pmShifts *vectors = psHashLookup(shifts, cellName);
+    if (!vectors) {
+        vectors = pmShiftsAlloc(tRel, xyRel);
+        psHashAdd(shifts, cellName, vectors);
+        psFree(vectors);            // Drop reference
+    }
+    return vectors;
+}
+
+
+bool pmShiftsRead(const pmCell *cell, psFits *fits)
+{
+    PS_ASSERT_PTR_NON_NULL(fits, false);
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+
+    bool mdok;                          // Status of MD lookup
+    pmShifts *check = psMetadataLookupPtr(&mdok, cell->analysis, PM_SHIFTS_TABLE_NAME); // Table, or NULL
+    if (check) {
+        psTrace(TRACE, 2, "Cell already has OT kernel present.\n");
+        return true;                    // No error
+    }
+    psTrace(TRACE, 2, "Reading FITS file for OT kernels.\n");
+
+    // Determine camera layout
+    pmChip *chip = cell->parent;        // The parent chip
+    pmFPA *fpa = chip->parent;          // The parent FPA
+    pmHDU *phu = NULL;                  // The primary header
+    pmFPALevel phuLevel = PM_FPA_LEVEL_NONE; // Level of the PHU
+    long numChips = 0;                  // Number of chips below the PHU; for setting efficient hash size
+    long numCells = 0;                  // Number of cells below the PHU; for setting efficient hash size
+    if (fpa->hdu) {
+        phu = fpa->hdu;
+        // Count the cells
+        psArray *chips = fpa->chips;    // Array of chips
+        numChips = chips->n;
+        for (int i = 0; i < chips->n; i++) {
+            pmChip *chip = chips->data[i]; // Chip of interest
+            numCells += chip->cells->n;
+        }
+        numCells = (float)numCells / (float)numChips + 0.5; // Average number of cells per chip
+        phuLevel = PM_FPA_LEVEL_FPA;
+    }
+    if (!phu && chip->hdu) {
+        phu = chip->hdu;
+        numChips = 1;
+        numCells = chip->cells->n;
+        phuLevel = PM_FPA_LEVEL_CHIP;
+    }
+    if (!phu && cell->hdu) {
+        phu = cell->hdu;
+        numChips = 0;
+        numCells = 1;
+        phuLevel = PM_FPA_LEVEL_CELL;
+    }
+    if (!phu || phuLevel == PM_FPA_LEVEL_NONE) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Can't find the PHU.\n");
+        return false;
+    }
+
+
+    // Find out what to read
+    psMetadata *shiftsInfo = psMetadataLookupMetadata(&mdok, phu->format, "SHIFTS"); // Shifts information
+    if (!mdok || !shiftsInfo) {
+        // We have read all the shifts that we have been told about --- which is none.
+        return true;
+    }
+    const char *shiftsExt = psMetadataLookupStr(&mdok, shiftsInfo, "EXTENSION"); // Extension name for shifts
+    if (!mdok || !shiftsExt) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Can't find EXTENSION name in SHIFTS information from camera format.");
+        return false;
+    }
+    const char *chipCol = NULL;         // Column name for chip
+    if (phuLevel == PM_FPA_LEVEL_FPA) {
+        chipCol = psMetadataLookupStr(&mdok, shiftsInfo, "CHIP");
+        if (!mdok || !chipCol) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                    "Can't find CHIP column name in SHIFTS information from camera format.");
+            return false;
+        }
+    }
+    const char *cellCol = NULL;         // Column name for cell
+    if (phuLevel <= PM_FPA_LEVEL_CHIP) {
+        cellCol = psMetadataLookupStr(&mdok, shiftsInfo, "CELL");
+        if (!mdok || !chipCol) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                    "Can't find CELL column name in SHIFTS information from camera format.");
+            return false;
+        }
+    }
+    const char *tCol = psMetadataLookupStr(&mdok, shiftsInfo, "T");
+    if (!mdok || !tCol) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Can't find T column name in SHIFTS information from camera format.");
+        return false;
+    }
+    const char *xCol = psMetadataLookupStr(&mdok, shiftsInfo, "X");
+    if (!mdok || !xCol) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Can't find X column name in SHIFTS information from camera format.");
+        return false;
+    }
+    const char *yCol = psMetadataLookupStr(&mdok, shiftsInfo, "Y");
+    if (!mdok || !yCol) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Can't find Y column name in SHIFTS information from camera format.");
+        return false;
+    }
+
+    bool tRel = psMetadataLookupBool(&mdok, shiftsInfo, "TRELATIVE");
+    if (!mdok) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Can't find TRELATIVE in SHIFTS information from camera format.");
+        return false;
+    };
+    bool xyRel = psMetadataLookupStr(&mdok, shiftsInfo, "XYRELATIVE");
+    if (!mdok) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Can't find XYRELATIVE in SHIFTS information from camera format.");
+        return false;
+    };
+
+    // Read the FITS file
+    int origExt = psFitsGetExtNum(fits); // Original extension number; to preserve position
+    if (!psFitsMoveExtName(fits, shiftsExt)) {
+        psError(PS_ERR_IO, false, "Unable to move to shifts extension %s", shiftsExt);
+        return false;
+    }
+    psArray *table = psFitsReadTable(fits); // The table of shifts
+    if (!table) {
+        psError(PS_ERR_IO, false, "Unable to read shifts table.\n");
+        return false;
+    }
+
+    // More sensible storage
+    pmShifts *singleShifts = NULL;      // Shifts for a single cell
+    psHash *multipleShifts = NULL;      // Shifts for multiple cells, stored by cell name
+    switch (phuLevel) {
+      case PM_FPA_LEVEL_FPA:
+        multipleShifts = psHashAlloc(2 * numChips);
+        break;
+      case PM_FPA_LEVEL_CHIP:
+        multipleShifts = psHashAlloc(2 * numCells);
+        break;
+      case PM_FPA_LEVEL_CELL:
+        singleShifts = pmShiftsAlloc(tRel, xyRel);
+        break;
+      default:
+        psAbort("Should never get here.\n");
+    }
+
+    // Pull values out of the table into something a bit more sensible
+    for (int i = 0; i < table->n; i++) {
+        psMetadata *row = table->data[i]; // The row of interest
+
+        const char *chipName = NULL;    // Name of chip
+        if (phuLevel == PM_FPA_LEVEL_FPA) {
+            // Only care about the chip name if there's more than one chip
+            chipName = psMetadataLookupStr(&mdok, row, chipCol);
+            if (!mdok || !chipName) {
+                psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find column %s in row %d of shifts table\n",
+                        chipCol, i);
+                psFree(multipleShifts);
+                psFree(table);
+                return false;
+            }
+        }
+        const char *cellName = NULL;    // Name of cell
+        if (phuLevel <= PM_FPA_LEVEL_CHIP) {
+            // Only care about the cell name if there's a chip
+            cellName = psMetadataLookupStr(&mdok, row, cellCol);
+            if (!mdok || !cellName) {
+                psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find column %s in row %d of shifts table\n",
+                        cellCol, i);
+                psFree(multipleShifts);
+                psFree(table);
+                return false;
+            }
+        }
+        float x = psMetadataLookupS32(&mdok, row, xCol); // Shift in x
+        if (!mdok) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find column %s in row %d of shifts table\n",
+                    xCol, i);
+            psFree(multipleShifts);
+            psFree(table);
+            return false;
+        }
+        float y = psMetadataLookupF32(&mdok, row, yCol); // Shift in y
+        if (!mdok) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find column %s in row %d of shifts table\n",
+                    yCol, i);
+            psFree(multipleShifts);
+            psFree(table);
+            return false;
+        }
+        float t = psMetadataLookupF32(&mdok, row, tCol); // Time of shift
+        if (!mdok) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find column %s in row %d of shifts table\n",
+                    tCol, i);
+            psFree(multipleShifts);
+            psFree(table);
+            return false;
+        }
+
+        pmShifts *shifts = NULL;        // Shifts for the cell of interest
+        switch (phuLevel) {
+          case PM_FPA_LEVEL_FPA: {
+              psHash *cells = psHashLookup(multipleShifts, chipName); // Hash of cells
+              if (!cells) {
+                  cells = psHashAlloc(numCells);
+                  psHashAdd(multipleShifts, chipName, cells);
+                  psFree(cells);          // Drop reference
+              }
+              shifts = cellVectors(cells, cellName, tRel, xyRel);
+              break;
+          }
+          case PM_FPA_LEVEL_CHIP:
+            shifts = cellVectors(multipleShifts, cellName, tRel, xyRel);
+            break;
+          case PM_FPA_LEVEL_CELL:
+            shifts = singleShifts;
+            break;
+          default:
+            psAbort("Should never get here.\n");
+        }
+
+        // Add the shift
+        psVectorExtend(shifts->x, 1, SHIFTS_BUFFER);
+        psVectorExtend(shifts->y, 1, SHIFTS_BUFFER);
+        psVectorExtend(shifts->t, 1, SHIFTS_BUFFER);
+        shifts->x->data.S32[shifts->num] = (int)x;
+        shifts->y->data.S32[shifts->num] = (int)y;
+        shifts->t->data.F32[shifts->num] = t;
+        shifts->num++;
+    }
+    psFree(table);
+
+    // Put the kernels into their own cells
+    if (phuLevel == PM_FPA_LEVEL_CELL) {
+        // Only a single cell
+        psMetadataAddPtr(cell->analysis, PS_LIST_TAIL, PM_SHIFTS_TABLE_NAME, PS_DATA_KERNEL,
+                         "Orthogonal transfer shifts", singleShifts);
+        psFree(singleShifts);
+        return true;
+    } else {
+        psList *names = psHashKeyList(multipleShifts); // List of hash keys (chip/cell names)
+        psListIterator *namesIter = psListIteratorAlloc(names, PS_LIST_HEAD, false); // Iterator for names
+        const char *name;               // Name, from iteration
+        while ((name = psListGetAndIncrement(namesIter))) {
+            switch (phuLevel) {
+              case PM_FPA_LEVEL_FPA: {
+                  int chipNum = pmFPAFindChip(fpa, name); // Number of chip of interest
+                  if (chipNum < 0) {
+                      psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find chip %s", name);
+                      psFree(namesIter);
+                      psFree(names);
+                      psFree(multipleShifts);
+                      return false;
+                  }
+                  pmChip *chip = fpa->chips->data[chipNum]; // Chip of interest
+
+                  // Loop over component cells
+                  psHash *cells = psHashLookup(multipleShifts, name); // Hash of cells
+                  psList *cellNames = psHashKeyList(multipleShifts); // List of hash keys (cell names)
+                  psListIterator *cellNamesIter = psListIteratorAlloc(cellNames, PS_LIST_HEAD, false);
+                  const char *cellName;       // Cell name, from iteration
+                  while ((cellName = psListGetAndIncrement(cellNamesIter))) {
+                      int cellNum = pmChipFindCell(chip, cellName); // Number of cell of interest
+                      if (!cell) {
+                          psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find cell %s", cellName);
+                          psFree(cellNamesIter);
+                          psFree(cellNames);
+                          psFree(namesIter);
+                          psFree(names);
+                          psFree(multipleShifts);
+                          return false;
+                      }
+                      pmCell *cell = chip->cells->data[cellNum]; // Cell of interest
+                      if (psMetadataLookup(cell->analysis, PM_SHIFTS_TABLE_NAME)) {
+                          // Already has a shifts table, for some reason
+                          psWarning("Chip %s, cell %s already has a shifts table --- overwriting\n",
+                                    name, cellName);
+                      }
+
+                      pmShifts *vectors = psHashLookup(cells, cellName); // Shifts for the cell of interest
+                      psMetadataAddPtr(cell->analysis, PS_LIST_TAIL, PM_SHIFTS_TABLE_NAME,
+                                       PS_DATA_KERNEL | PS_META_REPLACE,
+                                       "Orthogonal transfer shifts", vectors);
+                  }
+                  psFree(cellNamesIter);
+                  psFree(cellNames);
+                  break;
+              }
+              case PM_FPA_LEVEL_CHIP: {
+                  int cellNum = pmChipFindCell(chip, name); // Number of cell of interest
+                  if (!cell) {
+                      psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find cell %s", name);
+                      psFree(namesIter);
+                      psFree(names);
+                      psFree(multipleShifts);
+                      return false;
+                  }
+                  pmCell *cell = chip->cells->data[cellNum]; // Cell of interest
+                  if (psMetadataLookup(cell->analysis, PM_SHIFTS_TABLE_NAME)) {
+                      // Already has a shifts table, for some reason
+                      psWarning("Cell %s already has a shifts table --- overwriting\n", name);
+                  }
+
+                  pmShifts *vectors = psHashLookup(multipleShifts, name); // Shifts for this cell
+                  psMetadataAddPtr(cell->analysis, PS_LIST_TAIL, PM_SHIFTS_TABLE_NAME,
+                                   PS_DATA_KERNEL | PS_META_REPLACE,
+                                   "Orthogonal transfer shifts", vectors);
+                  break;
+              }
+              default:
+                psAbort("Should never get here.\n");
+            }
+        }
+        psFree(namesIter);
+        psFree(names);
+        psFree(multipleShifts);
+    }
+
+    // Go back to the original position in the FITS file
+    return psFitsMoveExtNum(fits, origExt, false);
+}
+
+
+// Generate a kernel and stuff it on the cell metadata
+bool pmShiftsKernel(const pmCell *cell     // Cell to which the shifts belong
+    )
+{
+    PS_ASSERT_PTR(cell, false);
+
+    bool mdok;                          // Status of MD lookup
+    pmShifts *shifts = psMetadataLookupPtr(&mdok, cell->analysis, PM_SHIFTS_TABLE_NAME);
+    if (!shifts) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find shifts table for cell.\n");
+        return false;
+    }
+
+    psKernel *kernel = psKernelGenerate(shifts->t, shifts->x, shifts->y,
+                                        shifts->tRelative, shifts->xyRelative); // Shift kernel
+    if (!kernel) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to generate kernel from OT shifts");
+        return false;
+    }
+    psMetadataAddPtr(cell->analysis, PS_LIST_TAIL, PM_SHIFTS_KERNEL_NAME, PS_DATA_KERNEL,
+                     "Orthogonal transfer kernel, calculated from shifts", kernel);
+    psFree(kernel);
+
+    return true;
+}
+
+bool pmShiftsConvolve(pmReadout *detrend, const pmCell *source, psMaskType maskVal)
+{
+    PS_ASSERT_PTR(detrend, false);
+    PS_ASSERT_PTR(source, false);
+
+    bool mdok;                          // Status of MD lookup
+    psKernel *kernel = psMetadataLookupPtr(&mdok, source->analysis, PM_SHIFTS_KERNEL_NAME);
+    if (!kernel) {
+        // Maybe they just forgot to generate the kernel with pmShiftsKernel
+        if (psMetadataLookup(source->analysis, PM_SHIFTS_TABLE_NAME)) {
+            if (!pmShiftsKernel(source)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to generate shifts kernel.");
+                return false;
+            }
+            // Hopefully it's there now
+            kernel = psMetadataLookupPtr(&mdok, source->analysis, PM_SHIFTS_KERNEL_NAME);
+            if (!kernel) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to find shifts kernel.");
+                return false;
+            }
+        } else {
+            psError(PS_ERR_UNKNOWN, false, "Unable to find shifts kernel or shifts table.");
+            return false;
+        }
+    }
+
+    if (detrend->image) {
+#if 1
+        // Always do direct convolution (no fuss with edge effects)
+        psImage *convolved = psImageConvolveDirect(NULL, detrend->image, kernel);
+#else
+        // Kernel size-dependent convolution --- if it's big, use the FFT
+        int xSize = kernel->xMax - kernel->xMin; // Kernel size in x
+        int ySize = kernel->yMax - kernel->yMin; // Kernel size in y
+        psImage *convolved;
+        if (xSize * ySize < FFT_SIZE * FFT_SIZE) {
+            convolved = psImageConvolveDirect(NULL, detrend->image, kernel);
+        } else {
+            // This is a little dodgy --- making choices about parameters without the user's input
+            psStats *stats = psImageStats(PS_STAT_ROBUST_MEDIAN);
+            stats->nSubsample = 10000;
+            psImageBackground(stats, detrend->image, detrend->mask, maskVal, NULL);
+            convolved = psImageConvolveFFT(detrend->image, kernel, stats->robustMedian);
+        }
+#endif
+        if (!convolved) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to convolve detrend image.");
+            return false;
+        }
+        psFree(detrend->image);
+        detrend->image = convolved;
+    }
+
+    // Purposely ignoring the weight map --- don't care about the weight map for a detrend image
+
+    if (maskVal && detrend->mask && !psImageConvolveMaskDirect(detrend->mask, detrend->mask, maskVal, 0,
+                                                               kernel->xMin, kernel->xMax,
+                                                               kernel->yMin, kernel->yMax)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to convolve detrend mask.");
+        return false;
+    }
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmShifts.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmShifts.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmShifts.h	(revision 15358)
@@ -0,0 +1,51 @@
+#ifndef PM_SHIFTS_H
+#define PM_SHIFTS_H
+
+#define PM_SHIFTS_TABLE_NAME "SHIFTS.TABLE" ///< Name for table on the analysis metadata
+#define PM_SHIFTS_KERNEL_NAME "SHIFTS.KERNEL" ///< Name for kernel on the analysis metadata
+
+/// Shifts due to orthogonal transfer
+typedef struct {
+    psVector *x;                        ///< Shifts in x
+    psVector *y;                        ///< Shifts in y
+    psVector *t;                        ///< Times of shifts
+    long num;                           ///< Number of values
+    bool tRelative;                     ///< Are the time values relative (durations)?
+    bool xyRelative;                    ///< Are the shift (x,y) values relative to the previous position?
+} pmShifts;
+
+/// Allocator for pmShifts
+pmShifts *pmShiftsAlloc(bool tRel,      ///< Are the time values relative (durations)?
+                        bool xyRel      ///< Are the shift (x,y) values relative to the previous position?
+                        );
+
+/// Read orthogonal transfer shifts table for a cell
+///
+/// Given a cell, this function searches for the orthogonal transfer shifts for this cell in the supplied FITS
+/// file.  The FITS extension containing the shifts table is specified by the SHIFTS keyword within the FILE
+/// information in the camera format.  If the extension is found, the shifts table is read and translated into
+/// kernels which are placed in the analysis metadata of each cell.  Note that this operation is performed on
+/// all cells within the FITS file (or at least, those contained within the shifts table), not just the cell
+/// provided --- if we have to read the whole table, we may as well translate the whole lot.  If a kernel is
+/// already present in the cell analysis metadata, the function returns true without doing any work.
+bool pmShiftsRead(const pmCell *cell,         ///< Cell for which to search for shifts
+                  psFits *fits          ///< FITS file in which to search for OT shifts extension
+                  );
+
+
+/// Generate a kernel for the cell from the orthogonal transfer shifts
+///
+/// The kernel is saved in the analysis metadata
+bool pmShiftsKernel(const pmCell *cell   ///< Cell for which to generate kernel
+                    );
+
+/// Convolve a detrend with the appropriate orthogonal transfer convolution kernel from a science exposure.
+///
+/// The kernel is generated (with pmShiftsKernel) if required.  The image and mask are convolved with the
+/// kernel (specified maskVal is smeared).  The weight map is not convolved.
+bool pmShiftsConvolve(pmReadout *detrend, ///< Detrend readout to convolve
+                      const pmCell *source, ///< Science exposure, containing a shifts kernel
+                      psMaskType maskVal ///< Mask value to smear
+                      );
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmShutterCorrection.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmShutterCorrection.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmShutterCorrection.c	(revision 15358)
@@ -0,0 +1,987 @@
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <strings.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+#include "psVectorBracket.h"
+#include "pmConceptsAverage.h"
+#include "pmReadoutStack.h"
+
+#include "pmShutterCorrection.h"
+
+/// Measure shutter correction:
+///
+/// input  : collection of shutter correction exposures (pre-processed)
+/// output : a shutter correction image
+///
+/// The measurement could be performed on any focal-plane unit at a time. for GPC, the obvious scale is to
+/// measure the effect on the entire focal plane at once, with a single reference point in the field.  this is
+/// a little more complex than just measuring the effect for a single 2D image array.  the reference point and
+/// the detailed analysis points need to be defined for the entire hierarchy rather than just as coordinate
+/// pairs or regions.  a pmFPAview would be a natural element with which to define these points, but at the
+/// moment, the pmFPAview structure defines a band in the CCD, not a coordinate.  An option is to instead
+/// specify the reference locations as a pmFPAview coupled with a psRegion, though we need to be careful not
+/// to over-specify the pixels (ie, conflict between pmFPAview and psRegion).
+///
+/// At each point in an image with exposure time T, we measure f(k;T) = F(k;T) / F(0;T) where k is the
+/// coordinate of the point of interest, 0 is the reference coordinate, and F(k;T) is the measured number of
+/// counts at the point of interest in this image.  given a collection of f(k;T) values, we need to determine
+/// the model f(k;T) = A(k) (T + dTk) / (T + dTo) where dTk is the shutter error at the given position, dTo is
+/// the shutter error at the reference position, and A(k) is the scaling factor for the given position.
+///
+/// The process for generating a shutter correction is as follows:
+/// - for each image
+/// -- measure the reference point counts
+/// - for each analysis region:
+/// -- measure shutter parameters (dTo, dTk, A):
+/// --- for each image:
+/// ---- measure counts at the region
+/// ---- divide by the reference counts
+/// --- linear extrapolation to find f(inf) = A(k)
+/// --- linear extrapolation to find f(0) = A(k) dTk / dTo
+/// --- linear interpolation to find coordinate where f(dTo) = A (1 + dTk/dTo) / 2
+/// --- non-linear fit of T, f(T) to f(k;T) = A(k) (T + dTk) / (T + dTo)
+/// - use the collection of dTo values to choose a best value for dTo (median)
+/// - for each image pixel
+/// -- divide by the reference counts
+/// -- generate the vectors T, f(T)
+/// -- linear fit of T, f(T) to f(k;T) = A(k) (T + dTk) / (T + dTo) using dTo above
+/// -- save dTk, A(k) in output image pixels
+/// -- apply dTk, A(k) to measure residual images
+/// -- generate residual FITS/JPEG images
+
+
+#define MEASURE_SAMPLES 4               // Number of samples to make over the image.  This should only be
+                                        // changed with great caution, since assumptions on its value are in
+                                        // the code (see pmShutterCorrectionDataAlloc).
+
+
+static void pmShutterCorrectionFree(pmShutterCorrection *pars)
+{
+    // Nothing to free
+    return;
+}
+
+pmShutterCorrection *pmShutterCorrectionAlloc()
+{
+    pmShutterCorrection *corr = (pmShutterCorrection*)psAlloc(sizeof(pmShutterCorrection));
+    psMemSetDeallocator(corr, (psFreeFunc)pmShutterCorrectionFree);
+
+    corr->scale  = 0.0;
+    corr->offset = 0.0;
+    corr->offref = 0.0;
+
+    return corr;
+}
+
+pmShutterCorrection *pmShutterCorrectionGuess(const psVector *exptime, const psVector *counts)
+{
+    // NOTE: vectors must be sorted on input.  It is expensive to sort or check this here, but
+    // it is easy to arrange by sorting the images before generating these vectors.
+
+    PS_ASSERT_VECTOR_NON_NULL(exptime, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(counts, NULL);
+    PS_ASSERT_VECTOR_TYPE(exptime, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_TYPE(counts, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(exptime, counts, NULL);
+    if (exptime->n <= 2) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                "Require more than 2 exposures to guess shutter correction.\n");
+        return NULL;
+    }
+
+    long N = exptime->n;                // Number of exposures
+
+    // use interpolation to guess shutter correction parameters given a set of exposures times and normalized
+    // counts (divided by the reference counts for each image)
+
+    pmShutterCorrection *corr = pmShutterCorrectionAlloc(); // Shutter correction, to be returned
+    psPolynomial1D *line = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 1); // Straight line, for extrapolation
+
+    // choose the highest exptime point as the guess for the scale:
+    // XXX we could examine the top 2 or 3 values and decide if we
+    // extended exptime enough or median clip.
+    corr->scale = counts->data.F32[N-1];
+
+    // fit a line to the lowest three points and extrapolate to 0.0
+    psVector *tmpX = psVectorAlloc(2, PS_TYPE_F32);
+    psVector *tmpY = psVectorAlloc(2, PS_TYPE_F32);
+
+    long index;
+    for (index = 0; !isfinite(exptime->data.F32[index]) && index < N - 1; index++)
+        ; // Iterate only
+    if (index == N - 1) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Not enough good values to guess shutter correction.\n");
+        goto GUESS_ERROR;
+    }
+    tmpX->data.F32[0] = exptime->data.F32[index];
+    tmpY->data.F32[0] = counts->data.F32[index];
+
+    for (index++;
+            (!isfinite(exptime->data.F32[index]) || exptime->data.F32[index] == exptime->data.F32[0]) &&
+            index < N; index++)
+        ; // Iterate only
+    if (index == N) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Exposure times are all identical --- cannot guess shutter correction.\n");
+        goto GUESS_ERROR;
+    }
+    tmpY->data.F32[1] = counts->data.F32[index];
+    tmpX->data.F32[1] = exptime->data.F32[index];
+
+    // fit a line and extrapolate the fit to 0.0
+    if (!psVectorFitPolynomial1D(line, NULL, 0, tmpY, NULL, tmpX)) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to fit for the time offset.\n");
+        goto GUESS_ERROR;
+    }
+    float ratio = psPolynomial1DEval(line, 0.0) / corr->scale;
+
+    // XXX we need a sanity check:
+    // if the mean value of the three points is higher than corr->scale,
+    // then the slope should be negative.
+    // if the mean value of the three points is lower than corr->scale,
+    // then the slope should be positive.
+
+    // find two points bracketing the value counts = A (1 + dTk/dTo) / 2 = corr->scale (1 + ratio) / 2
+    float value = corr->scale * (1 + ratio) / 2.0;
+
+    int Np;                             // Index of the value above (positive side)
+    if (ratio < 1.0) {
+        Np = psVectorBracket(counts, value, true);
+    } else {
+        Np = psVectorBracketDescend(counts, value, true);
+    }
+    int Nm = (Np == 0) ? 1 : Np - 1;    // Index of the value below (negative side)
+
+    tmpX->data.F32[0] = counts->data.F32[Nm];
+    tmpX->data.F32[1] = counts->data.F32[Np];
+    tmpY->data.F32[0] = exptime->data.F32[Nm];
+    tmpY->data.F32[1] = exptime->data.F32[Np];
+
+    // fit a line and extrapolate the fit to counts = A (1 + dTk/dTo) : exptime = dTo
+    if (!psVectorFitPolynomial1D (line, NULL, 0, tmpY, NULL, tmpX)) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to fit for the reference offset.\n");
+        goto GUESS_ERROR;
+    }
+    corr->offref = psPolynomial1DEval(line, value);
+    corr->offset = ratio * corr->offref;
+
+    psFree(line);
+    psFree(tmpX);
+    psFree(tmpY);
+
+    return corr;
+
+GUESS_ERROR:
+    psFree(tmpX);
+    psFree(tmpY);
+    psFree(line);
+    psFree(corr);
+    return NULL;
+}
+
+// linear fit to the counts and exptime, given a value for offref
+pmShutterCorrection *pmShutterCorrectionLinFit(const psVector *exptime,
+        const psVector *counts,
+        const psVector *cntError,
+        const psVector *mask,
+        float offref,
+        int nIter,
+        float rej,
+        psMaskType maskVal
+                                              )
+{
+    PS_ASSERT_VECTOR_NON_NULL(exptime, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(counts, NULL);
+    PS_ASSERT_VECTOR_TYPE(exptime, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_TYPE(counts, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(exptime, counts, NULL);
+    if (exptime->n <= 2) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                "Require more than 2 exposures to guess shutter correction.\n");
+        return NULL;
+    }
+    if (cntError) {
+        PS_ASSERT_VECTOR_TYPE(cntError, PS_TYPE_F32, NULL);
+        PS_ASSERT_VECTORS_SIZE_EQUAL(counts, cntError, NULL);
+    }
+    PS_ASSERT_FLOAT_LARGER_THAN(offref, 0.0, NULL);
+
+    // this step is identical for all pixels: do it once and save?
+    psVector *x = psVectorAlloc(exptime->n, PS_TYPE_F32);
+    psVector *y = psVectorAlloc(exptime->n, PS_TYPE_F32);
+
+    for (long i = 0; i < exptime->n; i++) {
+        // Should be safe (if expensive) to stick NaNs in --- the fitter deals with them
+        float value = 1.0 / (exptime->data.F32[i] + offref);
+        x->data.F32[i] = exptime->data.F32[i] * value;
+        y->data.F32[i] = value;
+    }
+
+    psPolynomial2D *line = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, 1, 1);
+
+    // mask out the terms we will not fit
+    line->coeffMask[0][0] = PS_POLY_MASK_SET;
+    line->coeffMask[1][1] = PS_POLY_MASK_SET;
+    line->coeff[0][0] = 0;
+    line->coeff[1][1] = 0;
+
+    // the stats structure determines how the clipping statistic is measured
+    // too few points to use the robust analysis method
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    stats->clipSigma = rej;
+    stats->clipIter = nIter;
+
+    if (!psVectorClipFitPolynomial2D(line, stats, mask, maskVal, counts, cntError, x, y)) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to fit shutter correction.\n");
+        psFree(stats);
+        psFree(x);
+        psFree(y);
+        psFree(line);
+        return NULL;
+    }
+    psFree(stats);
+
+    pmShutterCorrection *corr = pmShutterCorrectionAlloc();
+    corr->offref = offref;
+    corr->scale  = line->coeff[1][0];
+    corr->offset = line->coeff[0][1] / line->coeff[1][0];
+
+    psFree(x);
+    psFree(y);
+    psFree(line);
+
+    return corr;
+}
+
+static psF32 pmShutterCorrectionModel(psVector *deriv,
+                                      const psVector *params,
+                                      const psVector *x)
+{
+    // This is in a tight loop, so we won't assert here.
+
+    psF32 A = params->data.F32[0];
+    psF32 p = x->data.F32[0] + params->data.F32[1];
+    psF32 q = 1.0 / (x->data.F32[0] + params->data.F32[2]);
+    psF32 f = A * p * q;
+
+    if (deriv) {
+        deriv->data.F32[0] = p * q;
+        deriv->data.F32[1] = A * q;
+        deriv->data.F32[2] = - f * q;
+    }
+    return f;
+}
+
+// non-linear fit to the counts and exptime, given a guess for the three parameters
+pmShutterCorrection *pmShutterCorrectionFullFit(const psVector *exptime,
+        const psVector *counts,
+        const psVector *cntError,
+        const pmShutterCorrection *guess
+                                               )
+{
+    PS_ASSERT_VECTOR_NON_NULL(exptime, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(counts, NULL);
+    PS_ASSERT_VECTOR_TYPE(exptime, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_TYPE(counts, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(exptime, counts, NULL);
+    if (exptime->n <= 2) {
+        psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                "Require more than 2 exposures to guess shutter correction.\n");
+        return NULL;
+    }
+    if (cntError) {
+        PS_ASSERT_VECTOR_TYPE(cntError, PS_TYPE_F32, NULL);
+        PS_ASSERT_VECTORS_SIZE_EQUAL(counts, cntError, NULL);
+    }
+    PS_ASSERT_PTR_NON_NULL(guess, NULL);
+
+    psMinimization *minInfo = psMinimizationAlloc(15, 0.1); // Minimization information
+
+    psVector *params = psVectorAlloc (3, PS_TYPE_F32); // Fitting parameters
+    params->data.F32[0] = guess->scale;
+    params->data.F32[1] = guess->offset;
+    params->data.F32[2] = guess->offref;
+
+    // XXX for the moment, don't set any constraints
+    // psMinConstraint *constraint = psMinConstraintAlloc();
+    // constrain->checkLimits = pmShutterParamLimits;
+    // constrain->paramMask   = NULL;
+    psMinConstraint *constraint = NULL;   // Constraints on the minimization
+
+    // XXX ignore covariance matrix for the moment
+    // psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F64);
+    psImage *covar = NULL;              // Covariance matrix
+
+    // construct the coordinate and value entries (y is counts)
+    psArray *x = psArrayAlloc(exptime->n); // Coordinates
+
+    for (long i = 0; i < exptime->n; i++) {
+        psVector *coord = psVectorAlloc(1, PS_TYPE_F32);
+        coord->data.F32[0] = exptime->data.F32[i];
+        x->data[i] = coord;
+    }
+
+    if (!psMinimizeLMChi2(minInfo, covar, params, constraint, x, counts, cntError, pmShutterCorrectionModel)) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to fit for shutter correction.\n");
+        psFree(x);
+        psFree(minInfo);
+        psFree(params);
+        return NULL;
+    }
+
+    pmShutterCorrection *corr = pmShutterCorrectionAlloc(); // Shutter correction
+    corr->scale  = params->data.F32[0];
+    corr->offset = params->data.F32[1];
+    corr->offref = params->data.F32[2];
+
+    psFree(minInfo);
+    psFree(params);
+    psFree(x);
+
+    return corr;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmShutterCorrectionMeasure(pmReadout *output, const psArray *readouts, int size, psStatsOptions meanStat,
+                                psStatsOptions stdevStat, int nIter, float rej, psMaskType maskVal)
+{
+    PS_ASSERT_ARRAY_NON_NULL(readouts, NULL);
+    PS_ASSERT_ARRAY_NON_EMPTY(readouts, NULL);
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+
+    long num = readouts->n;             // Number of readouts
+    PS_ASSERT_INT_POSITIVE(nIter, NULL);
+    PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, NULL);
+
+    psArray *images = psArrayAlloc(num);// Array of images
+    psArray *masks = NULL; // Array of masks
+    psArray *weights = NULL; // Array of weights
+    psVector *exptimes = psVectorAlloc(num, PS_TYPE_F32); // Vector of exposure times
+
+    {
+        pmReadout *readout = readouts->data[0]; // Representative readout
+        if (readout->mask)
+        {
+            masks = psArrayAlloc(num);
+        }
+        if (readout->weight)
+        {
+            weights = psArrayAlloc(num);
+        }
+    }
+
+    // Check input sizes, generate first-pass statistics
+    psRegion refRegion;                 // Reference region
+    psVector *refs = psVectorAlloc(num, PS_TYPE_F32); // Reference measurements
+    psVectorInit(refs, 0);
+    psArray *regions = psArrayAlloc(MEASURE_SAMPLES); // Array of sample regions, made on each image
+    psImage *samplesMean = psImageAlloc(num, MEASURE_SAMPLES, PS_TYPE_F32); // Measurements for each file
+    psImage *samplesStdev = psImageAlloc(num, MEASURE_SAMPLES, PS_TYPE_F32); // Errors for each file
+    psStats *stats = psStatsAlloc(meanStat | stdevStat);
+    int numRows = 0, numCols = 0; // Size of images
+    for (long i = 0; i < images->n; i++) {
+        pmReadout *readout = readouts->data[i]; // Readout of interest
+        if (!readout) {
+            continue;
+        }
+
+        bool mdok;                      // Status of MD lookup
+        float exptime = psMetadataLookupF32(&mdok, readout->parent->concepts, "CELL.EXPOSURE"); // Exp. time
+        if (!mdok || !isfinite(exptime)) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Exposure time for readout %ld is not set.\n", i);
+            goto MEASURE_ERROR;
+        }
+        exptimes->data.F32[i] = exptime;
+
+        psImage *image = readout->image; // Image of interest
+        if (!image) {
+            continue;
+        }
+        images->data[i] = psMemIncrRefCounter(image);
+        if (image->type.type != PS_TYPE_F32) {
+            psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Bad type for image: %x\n", image->type.type);
+            goto MEASURE_ERROR;
+        }
+        if (numRows == 0 || numCols == 0) {
+            numRows = image->numRows;
+            numCols = image->numCols;
+            // Set up the sample regions
+            refRegion = psRegionForSquare(0.5 * numCols, 0.5 * numRows, size);
+            for (int j = 0; j < MEASURE_SAMPLES; j++) {
+                int x = (j % 2) ? size : image->numCols - size;
+                int y = (j > 1) ? size : image->numRows - size;
+                psRegion region = psRegionForSquare(x, y, size);
+                region = psRegionForImage(image, region);
+                regions->data[j] = psRegionAlloc(region.x0, region.x1, region.y0, region.y1);
+            }
+        } else if (numRows != image->numRows || numCols != image->numCols) {
+            psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                    "Image sizes don't match: %dx%d vs %dx%d\n", image->numCols, image->numRows,
+                    numCols, numRows);
+            goto MEASURE_ERROR;
+        }
+        psImage *mask = readout->mask; // Mask of interest
+        if (mask) {
+            if (!masks) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Not all readouts have masks.\n");
+                goto MEASURE_ERROR;
+            }
+            masks->data[i] = psMemIncrRefCounter(mask);
+
+            if (mask->type.type != PS_TYPE_U8) {
+                psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Bad type for mask: %x\n", mask->type.type);
+                goto MEASURE_ERROR;
+            }
+            if (mask->numRows != numRows || mask->numCols != numCols) {
+                psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                        "Mask sizes don't match: %dx%d vs %dx%d\n", mask->numCols, mask->numRows,
+                        numCols, numRows);
+                goto MEASURE_ERROR;
+            }
+        } else if (masks) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Not all readouts have masks.\n");
+            goto MEASURE_ERROR;
+        }
+
+        psImage *weight = readout->weight; // Weight map of interest
+        if (weight) {
+            if (!weights) {
+                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Not all readouts have weights.\n");
+                goto MEASURE_ERROR;
+            }
+            weights->data[i] = psMemIncrRefCounter(weight);
+
+            if (weight->type.type != PS_TYPE_F32) {
+                psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Bad type for weights: %x\n", weight->type.type);
+                goto MEASURE_ERROR;
+            }
+            if (weight->numRows != numRows || weight->numCols != numCols) {
+                psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+                        "Weight sizes don't match: %dx%d vs %dx%d\n", weight->numCols, weight->numRows,
+                        numCols, numRows);
+                goto MEASURE_ERROR;
+            }
+        } else if (weights) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Not all readouts have weights.\n");
+            goto MEASURE_ERROR;
+        }
+
+
+        // Measure statistics
+        if (!psImageStats(stats, image, mask, maskVal)) {
+            psWarning("Unable to measure reference statistics.\n");
+        }
+        refs->data.F32[i] = psStatsGetValue(stats, meanStat);
+        psTrace("psModules.detrend", 3, "Reference value for image %ld = %f\n", i, refs->data.F32[i]);
+        if (refs->data.F32[i] <= 0.0) {
+            psError(PS_ERR_UNKNOWN, true, "Measured non-positive reference value.\n");
+            goto MEASURE_ERROR;
+        }
+        refs->data.F32[i] = 1.0 / refs->data.F32[i];
+        for (int j = 0; j < MEASURE_SAMPLES; j++) {
+            psRegion *region = regions->data[j]; // Region of interest
+            psImage *subImage = psImageSubset(image, *region); // Sub-image
+            psImage *subMask = NULL;
+            if (mask) {
+                subMask = psImageSubset(mask, *region);
+            }
+            if (!psImageStats(stats, subImage, subMask, maskVal)) {
+                psString regionString = psRegionToString(*region);
+                psWarning("Unable to measure sample statistics at %s in image %ld.\n",
+                          regionString, i);
+                psFree(regionString);
+            }
+            psFree(subImage);
+            samplesMean->data.F32[j][i] = psStatsGetValue(stats, meanStat) * refs->data.F32[i];
+            samplesStdev->data.F32[j][i] = psStatsGetValue(stats, stdevStat) * refs->data.F32[i];
+            psTrace("psModules.detrend", 5, "Image %ld, sample %d: %f +/- %f\n", i, j,
+                    samplesMean->data.F32[j][i], samplesStdev->data.F32[j][i]);
+        }
+    }
+    psFree(regions);
+    psFree(stats);
+
+    float meanRef = 0.0;                // Mean reference offset
+    int numGood = 0;                    // Number of good measurements
+    psVector *counts = psVectorAlloc(num, PS_TYPE_F32); // Mean for each image
+    psVector *errors = psVectorAlloc(num, PS_TYPE_F32); // Stdev for each image
+    for (int i = 0; i < MEASURE_SAMPLES; i++) {
+        counts = psImageRow(counts, samplesMean, i);
+        errors = psImageRow(errors, samplesStdev, i);
+        pmShutterCorrection *guess = pmShutterCorrectionGuess(exptimes, counts); // Guess at correction
+        pmShutterCorrection *corr = pmShutterCorrectionFullFit(exptimes, counts, errors, guess); // Correct'n
+        if (!corr) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to measure shutter reference correction.\n");
+            psFree(guess);
+            psFree(counts);
+            psFree(errors);
+            goto MEASURE_ERROR;
+        }
+        psTrace("psModules.detrend", 5, "Sample reference value: %f\n", corr->offref);
+        if (isfinite(corr->offref)) {
+            meanRef += corr->offref;
+            numGood++;
+        }
+        psFree(corr);
+        psFree(guess);
+    }
+    psFree(samplesMean);
+    psFree(samplesStdev);
+
+    if (numGood == 0) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to measure mean reference offset.\n");
+        psFree(counts);
+        psFree(errors);
+        goto MEASURE_ERROR;
+    }
+    meanRef /= (float)numGood;
+    psTrace("psModules.detrend", 3, "Mean reference value: %f\n", meanRef);
+
+    // Check the weights
+    if (weights && nIter > 1) {
+        for (int i = 0; i < weights->n && nIter > 1; i++) {
+            psImage *weight = weights->data[i]; // Weight image
+            if (!weight) {
+                // We don't have weights, so no realistic errors: turn off iteration
+                if (nIter > 0) {
+                    psWarning("Not all images have weights --- turning iteration off.\n");
+                }
+                nIter = 1;
+            }
+        }
+    }
+
+    psImage *shutter = psImageAlloc(numCols, numRows, PS_TYPE_F32); // Shutter correction image
+    psImage *pattern = psImageAlloc(numCols, numRows, PS_TYPE_F32); // Illumination pattern
+    psVector *mask = psVectorAlloc(num, PS_TYPE_U8); // Mask for each image
+    psVectorInit(mask, 0);
+    psTrace("psModules.detrend", 2, "Performing linear fit on individual pixels...\n");
+    for (int y = 0; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            for (int i = 0; i < num; i++) {
+                psImage *image = images->data[i]; // Image of interest
+                counts->data.F32[i] = image->data.F32[y][x] * refs->data.F32[i];
+                psImage *maskImage;     // Mask image
+                if (masks && (maskImage = masks->data[i])) {
+                    mask->data.U8[i] = maskImage->data.U8[y][x];
+                }
+                psImage *weight;        // Weight image
+                if (weights && (weight = weights->data[i])) {
+                    errors->data.F32[i] = sqrtf(weight->data.F32[y][x]) * refs->data.F32[i];
+                } else {
+                    errors->data.F32[i] = sqrtf(image->data.F32[y][x]) * refs->data.F32[i];
+                }
+            }
+
+            pmShutterCorrection *corr = pmShutterCorrectionLinFit(exptimes, counts, errors, mask, meanRef,
+                                        nIter, rej, maskVal);
+            shutter->data.F32[y][x] = corr->offset;
+            pattern->data.F32[y][x] = corr->scale;
+            psFree(corr);
+        }
+    }
+    psFree(mask);
+    psFree(counts);
+    psFree(errors);
+    psFree(refs);
+
+    if (psTraceGetLevel("psModules.detrend") > 5) {
+        psFits *fits = psFitsOpen("pattern.fits", "w");
+        psFitsWriteImage(fits, NULL, pattern, 0, NULL);
+        psFitsClose(fits);
+    }
+    psFree(pattern);
+
+    output->image = shutter;
+
+    // Update the "concepts"
+    psList *inputCells = psListAlloc(NULL); // List of cells
+    for (long i = 0; i < readouts->n; i++) {
+        pmReadout *readout = readouts->data[i]; // Readout of interest
+        psListAdd(inputCells, PS_LIST_TAIL, readout->parent);
+    }
+    bool success = pmConceptsAverageCells(output->parent, inputCells, NULL, NULL, true);
+    psFree(inputCells);
+
+    // Correct the exposure times --- they don't make sense any more.
+    psMetadataItem *item = psMetadataLookup(output->parent->concepts, "CELL.EXPOSURE");
+    item->data.F32 = NAN;
+    item = psMetadataLookup(output->parent->concepts, "CELL.DARKTIME");
+    item->data.F32 = NAN;
+
+    return success;
+
+
+MEASURE_ERROR:
+    // Clean up after error
+    psFree(exptimes);
+    psFree(images);
+    psFree(masks);
+    psFree(weights);
+    psFree(refs);
+    psFree(regions);
+    psFree(stats);
+    psFree(samplesMean);
+    psFree(samplesStdev);
+    return false;
+}
+
+
+bool pmShutterCorrectionApply(pmReadout *readout, const pmReadout *shutter)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(shutter, false);
+    PS_ASSERT_IMAGE_NON_NULL(readout->image, false);
+    PS_ASSERT_IMAGE_NON_NULL(shutter->image, false);
+    PS_ASSERT_IMAGE_TYPE(readout->image, PS_TYPE_F32, false);
+    PS_ASSERT_IMAGE_TYPE(shutter->image, PS_TYPE_F32, false);
+
+    psRegion region = psRegionSet(readout->col0, readout->col0 + readout->image->numCols,
+                                  readout->row0, readout->row0 + readout->image->numRows); // Detector region
+
+    pmCell *cell = readout->parent;     // Parent cell
+    if (!cell) {
+        psError(PS_ERR_BAD_PARAMETER_NULL, true,
+                "Parent cell is NULL --- unable to determine exposure time.\n");
+        return false;
+    }
+    float exptime = psMetadataLookupF32(NULL, cell->concepts, "CELL.EXPOSURE"); // Exposure time
+    if (!isfinite(exptime)) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Bad exposure time: %f.\n", exptime);
+        return false;
+    }
+
+    pmHDU *hdu = pmHDUFromCell(cell);// HDU  of interest
+
+    psVector *md5 = psImageMD5(shutter->image); // md5 hash
+    psString md5string = psMD5toString(md5); // String
+    psFree(md5);
+    psStringPrepend(&md5string, "Shutter image MD5: ");
+    psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, md5string, "");
+    psFree(md5string);
+
+
+    psImage *shutterImage = psImageSubset(shutter->image, region); // Subimage with shutter
+    if (!shutterImage) {
+        psString regionString = psRegionToString(region);
+        psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Size mismatch: %s vs %dx%d\n",
+                regionString, shutter->image->numCols, shutter->image->numRows);
+        psFree(regionString);
+        psFree(shutterImage);
+        return false;
+    }
+    psImage *image = readout->image;    // Image to correct
+
+    if (exptime <= 0.0) {
+        // In the extreme case that we have exptime <= 0.0, we correct the image to
+        // counts-per-second, rather than counts in the nominal exposure time
+        for (int y = 0; y < image->numRows; y++) {
+            for (int x = 0; x < image->numCols; x++) {
+                image->data.F32[y][x] *= 1.0 / (exptime + shutterImage->data.F32[y][x]);
+            }
+        }
+        psMetadataAddF32 (cell->concepts, PS_LIST_TAIL, "CELL.EXPOSURE", PS_META_REPLACE, "exposure time re-normalized to 1.0", 1.0); // Exposure time
+        psString line = NULL;
+        psStringAppend (&line, "extreme exposure time %f, re-normalized to 1.0", exptime);
+        psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, line, "");
+        psFree (line);
+    } else {
+        for (int y = 0; y < image->numRows; y++) {
+            for (int x = 0; x < image->numCols; x++) {
+                image->data.F32[y][x] *= exptime / (exptime + shutterImage->data.F32[y][x]);
+            }
+        }
+    }
+    psFree(shutterImage);
+
+    psTime *time = psTimeGetNow(PS_TIME_TAI); // The time now, used for reporting
+    psString timeString = psTimeToISO(time); // String with time
+    psFree(time);
+    psStringPrepend(&timeString, "Shutter correction completed at ");
+    psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+                     timeString, "");
+    psFree(timeString);
+
+
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+#define IMAGES_BUFFER 10                // Allocate space for this many images at a time
+
+static void shutterCorrectionDataFree(pmShutterCorrectionData *data)
+{
+    psFree(data->regions);
+    psFree(data->mean);
+    psFree(data->stdev);
+
+    psFree(data->exptimes);
+    psFree(data->refs);
+
+    return;
+}
+
+pmShutterCorrectionData *pmShutterCorrectionDataAlloc(int numCols, int numRows, int size)
+{
+    pmShutterCorrectionData *data = psAlloc(sizeof(pmShutterCorrectionData));
+    psMemSetDeallocator(data, (psFreeFunc)shutterCorrectionDataFree);
+
+    data->num = 0;
+    data->numCols = 0;
+    data->numRows = 0;
+
+    data->regions = psArrayAlloc(MEASURE_SAMPLES);
+    for (int j = 0; j < MEASURE_SAMPLES; j++) {
+        int x = (j % 2) ? size : numCols - size - 1;
+        int y = (j > 1) ? size : numRows - size - 1;
+        psRegion region = psRegionForSquare(x, y, size);
+        data->regions->data[j] = psRegionAlloc(region.x0, region.x1, region.y0, region.y1);
+    }
+
+    data->mean = psArrayAlloc(MEASURE_SAMPLES);
+    data->stdev = psArrayAlloc(MEASURE_SAMPLES);
+    for (int i = 0; i < MEASURE_SAMPLES; i++) {
+        data->mean->data[i] = psVectorAllocEmpty(IMAGES_BUFFER, PS_TYPE_F32);
+        data->stdev->data[i] = psVectorAllocEmpty(IMAGES_BUFFER, PS_TYPE_F32);
+    }
+
+    data->exptimes = psVectorAllocEmpty(IMAGES_BUFFER, PS_TYPE_F32);
+    data->refs = psVectorAllocEmpty(IMAGES_BUFFER, PS_TYPE_F32);
+
+    return data;
+}
+
+bool pmShutterCorrectionAddReadout(pmShutterCorrectionData *data,
+                                   const pmReadout *readout, ///< Readout to add
+                                   psStatsOptions meanStat, ///< Statistic to use for mean
+                                   psStatsOptions stdevStat, ///< Statistic to use for stdev
+                                   psMaskType maskVal, ///< Mask value
+                                   psRandom *rng ///< Random number generator
+    )
+{
+    PS_ASSERT_PTR_NON_NULL(data, NULL);
+    PS_ASSERT_PTR_NON_NULL(readout, NULL);
+    PS_ASSERT_IMAGE_NON_NULL(readout->image, NULL);
+    PS_ASSERT_IMAGE_TYPE(readout->image, PS_TYPE_F32, NULL);
+    if (data->num == 0) {
+        data->numCols = readout->image->numCols;
+        data->numRows = readout->image->numRows;
+    } else {
+        PS_ASSERT_IMAGE_SIZE(readout->image, data->numCols, data->numRows, NULL);
+    }
+    if (readout->mask) {
+        PS_ASSERT_IMAGE_NON_NULL(readout->mask, NULL);
+        PS_ASSERT_IMAGE_TYPE(readout->mask, PS_TYPE_MASK, NULL);
+        PS_ASSERT_IMAGE_SIZE(readout->mask, data->numCols, data->numRows, NULL);
+    }
+    if (readout->weight) {
+        PS_ASSERT_IMAGE_NON_NULL(readout->weight, NULL);
+        PS_ASSERT_IMAGE_TYPE(readout->weight, PS_TYPE_F32, NULL);
+        PS_ASSERT_IMAGE_SIZE(readout->weight, data->numCols, data->numRows, NULL);
+    }
+
+    // Add the exposure time
+    float exptime = psMetadataLookupF32(NULL, readout->parent->concepts, "CELL.EXPOSURE"); // Exp. time
+    if (!isfinite(exptime)) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Exposure time is not set.");
+        return false;
+    }
+    data->exptimes->data.F32[data->exptimes->n] = exptime;
+    data->exptimes = psVectorExtend(data->exptimes, IMAGES_BUFFER, 1);
+
+    // Add the statistics
+
+    // Add the reference value
+    psStats *stats = psStatsAlloc(meanStat | stdevStat); // Statistics to apply
+    if (!rng) {
+        rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+    } else {
+        psMemIncrRefCounter(rng);
+    }
+    if (!psImageBackground(stats, NULL, readout->image, readout->mask, maskVal, rng)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to measure reference statistics.\n");
+        psFree(stats);
+        psFree(rng);
+        return false;
+    }
+    psFree(rng);
+    float refValue = psStatsGetValue(stats, meanStat); // Reference value
+    psTrace("psModules.detrend", 3, "Reference value for shutter image = %f\n", refValue);
+    if (refValue <= 0.0) {
+        psError(PS_ERR_UNKNOWN, true, "Measured non-positive reference value.\n");
+        psFree(stats);
+        return false;
+    }
+    refValue = 1.0 / refValue;
+    data->refs->data.F32[data->refs->n] = refValue;
+    data->refs = psVectorExtend(data->refs, IMAGES_BUFFER, 1);
+
+    // Add the region statistics
+    for (int j = 0; j < MEASURE_SAMPLES; j++) {
+        psRegion *region = data->regions->data[j]; // Region of interest
+        psImage *subImage = psImageSubset(readout->image, *region); // Sub-image
+        psImage *subMask = NULL;        // Sub-image of mask
+        if (readout->mask) {
+            subMask = psImageSubset(readout->mask, *region);
+        }
+        if (!psImageStats(stats, subImage, subMask, maskVal)) {
+            psString regionString = psRegionToString(*region);
+            psWarning("Unable to measure sample statistics at %s in image.\n",
+                      regionString);
+            psFree(regionString);
+        }
+        psFree(subImage);
+        psFree(subMask);
+
+        psVector *mean = data->mean->data[j]; // Vector of means for this region
+        psVector *stdev = data->stdev->data[j]; // Vector of standard deviations for this region
+
+        mean->data.F32[mean->n] = psStatsGetValue(stats, meanStat) * refValue;
+        stdev->data.F32[stdev->n] = psStatsGetValue(stats, stdevStat) * refValue;
+        data->mean->data[j] = psVectorExtend(mean, IMAGES_BUFFER, 1);
+        data->stdev->data[j] = psVectorExtend(stdev, IMAGES_BUFFER, 1);
+    }
+
+    data->num++;
+
+    return true;
+}
+
+float pmShutterCorrectionReference(const pmShutterCorrectionData *data)
+{
+    PS_ASSERT_PTR_NON_NULL(data, NAN);
+    PS_ASSERT_INT_POSITIVE(data->num, NAN);
+
+    float meanRef = 0.0;                // Mean reference offset
+    int numGood = 0;                    // Number of good measurements
+    for (int i = 0; i < MEASURE_SAMPLES; i++) {
+        psVector *counts = data->mean->data[i]; // Mean for each image
+        psVector *errors = data->stdev->data[i]; // Stdev for each image
+        pmShutterCorrection *guess = pmShutterCorrectionGuess(data->exptimes, counts); // Guess at correction
+        pmShutterCorrection *corr = pmShutterCorrectionFullFit(data->exptimes, counts,
+                                                               errors, guess); // The actual correction
+        psFree(guess);
+        if (corr && isfinite(corr->offref)) {
+            psTrace("psModules.detrend", 5, "Sample reference value: %f\n", corr->offref);
+            meanRef += corr->offref;
+            numGood++;
+        }
+        psFree(corr);
+    }
+
+    if (numGood == 0) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to measure mean reference offset.\n");
+        return false;
+    }
+    meanRef /= (float)numGood;
+    psTrace("psModules.detrend", 3, "Mean reference value: %f\n", meanRef);
+    return meanRef;
+}
+
+bool pmShutterCorrectionGenerate(pmReadout *shutter, pmReadout *pattern, const psArray *inputs,
+                                 float reference, const pmShutterCorrectionData *data,
+                                 int nIter, float rej, psMaskType maskVal)
+{
+    PS_ASSERT_PTR_NON_NULL(shutter, false);
+    PS_ASSERT_ARRAY_NON_NULL(inputs, false);
+    PS_ASSERT_INT_EQUAL(data->num, inputs->n, false);
+
+    int minInputCols, maxInputCols, minInputRows, maxInputRows; // Smallest and largest values to combine
+    int xSize, ySize;                   // Size of the output image
+    if (!pmReadoutStackValidate(&minInputCols, &maxInputCols, &minInputRows, &maxInputRows, &xSize, &ySize,
+                                inputs)) {
+        psError(PS_ERR_UNKNOWN, false, "No valid input readouts.");
+        return false;
+    }
+
+    pmReadoutUpdateSize(shutter, minInputCols, minInputRows, xSize, ySize, false);
+    if (pattern) {
+        pmReadoutUpdateSize(pattern, minInputCols, minInputRows, xSize, ySize, false);
+    }
+
+    psImage *shutterImage = shutter->image; // Shutter correction image
+    psImage *patternImage; // Illumination pattern
+    if (pattern) {
+        patternImage = psMemIncrRefCounter(pattern->image);
+    } else {
+        patternImage = psImageAlloc(xSize, ySize, PS_TYPE_F32);
+    }
+
+    int num = data->num;                // Number of images
+    psVector *counts = psVectorAlloc(num, PS_TYPE_F32); // Counts in each image
+    psVector *errors = psVectorAlloc(num, PS_TYPE_F32); // Counts in each image
+    psVector *mask = psVectorAlloc(num, PS_TYPE_MASK); // Mask for each image
+    psTrace("psModules.detrend", 2, "Performing linear fit on individual pixels...\n");
+    for (int i = minInputRows; i < maxInputRows; i++) {
+        int yOut = i - shutter->row0; // y position on output readout
+        for (int j = minInputCols; j < maxInputCols; j++) {
+            int xOut = j - shutter->col0; // x position on output readout
+
+            psVectorInit(mask, 0);
+            for (int r = 0; r < num; r++) {
+                pmReadout *readout = inputs->data[r]; // Readout of interest
+                int yIn = i - readout->row0; // y position on input readout
+                int xIn = j - readout->col0; // x position on input readout
+                psImage *image = readout->image; // Image of interest
+                float ref = data->refs->data.F32[r]; // (Inverse) reference value
+                counts->data.F32[r] = image->data.F32[yIn][xIn] * ref;
+                if (readout->mask) {
+                    mask->data.PS_TYPE_MASK_DATA[r] = readout->mask->data.PS_TYPE_MASK_DATA[yIn][xIn];
+                }
+                if (readout->weight) {
+                    errors->data.F32[r] = sqrtf(readout->weight->data.F32[yIn][xIn]) * ref;
+                } else {
+                    errors->data.F32[r] = sqrtf(image->data.F32[yIn][xIn]) * ref;
+                }
+            }
+
+            pmShutterCorrection *corr = pmShutterCorrectionLinFit(data->exptimes, counts, errors, mask,
+                                                                  reference, nIter, rej, maskVal);
+            shutterImage->data.F32[yOut][xOut] = corr->offset;
+            patternImage->data.F32[yOut][xOut] = corr->scale;
+            psFree(corr);
+        }
+    }
+    psFree(mask);
+    psFree(errors);
+    psFree(counts);
+
+    // Update the "concepts"
+    psList *inputCells = psListAlloc(NULL); // List of cells
+    for (long i = 0; i < inputs->n; i++) {
+        pmReadout *readout = inputs->data[i]; // Readout of interest
+        psListAdd(inputCells, PS_LIST_TAIL, readout->parent);
+    }
+    bool success = pmConceptsAverageCells(shutter->parent, inputCells, NULL, NULL, true);
+    psFree(inputCells);
+
+    // Correct the exposure times --- they don't make sense any more.
+    psMetadataItem *item = psMetadataLookup(shutter->parent->concepts, "CELL.EXPOSURE");
+    item->data.F32 = NAN;
+    item = psMetadataLookup(shutter->parent->concepts, "CELL.DARKTIME");
+    item->data.F32 = NAN;
+
+    shutter->data_exists = true;
+    shutter->parent->data_exists = true;
+    shutter->parent->parent->data_exists = true;
+
+    if (pattern) {
+        pattern->data_exists = true;
+        pattern->parent->data_exists = true;
+        pattern->parent->parent->data_exists = true;
+    }
+
+    return success;
+}
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmShutterCorrection.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmShutterCorrection.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmShutterCorrection.h	(revision 15358)
@@ -0,0 +1,186 @@
+/* @file pmShutterCorrection.h
+ * @brief Functions to build and apply a shutter exposure-time correction.
+ *
+ * @author Eugene Magnier, IfA
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.14 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-06-19 03:40:48 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_SHUTTER_CORRECTION_H
+#define PM_SHUTTER_CORRECTION_H
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+/*  A mechanical shutter may not yield uniform exposure times as a function of
+ *  position on the detector.  The typical error consists of a constant
+ *  exposure-time offset relative to the requested value, ie exposure time is
+ *  T_o + dT(x,y).  The exposure error, dT, may be measured with the following
+ *  scheme.  Obtain a set of exposures with different exposures times taken of
+ *  the same flat-field source; the source must be spatially stable between the
+ *  exposures, but need not have a stable amplitude.  For an illuminating flux
+ *  of intensity F(x,y) = F_o f(x,y), the signal recorded by any pixel in the
+ *  detector is given by: S(t,x,y) = F_o(t) f(x,y) (T_o + dT(x,y)) where F_o is
+ *  the F_o(t) is the (variable) overall intensity of the illuminating source
+ *  and f(x,y) is the spatial illumination patter times the flat-field response.
+ *  Choose a reference location in the image (eg, the detector center) and
+ *  divide by the value of that region (ie, mean or median):
+ *
+ *  s(t,x,y) = S(t,x,y) / S(t,0,0)
+ *  s(t,x,y) = F_o(t) f(x,y) (T_o + dT(x,y)) / F_o(t) f(0,0) (T_o + dT(0,0))
+ *  s(t,x,y) = f(x,y) (T_o + dT(x,y)) / f(0,0) (T_o + dT(0,0))
+ *
+ *  we can absorb the term f(0,0) into f(x,y) as we have no motivation for the
+ *  scale of f(x,y).  For any single pixel, over the set of exposures, we thus
+ *  need to solve for dT(x,y), dT(0,0), and f'(x,y) in the equation:
+ *  s(t,x,y) = f'(x,y) (T_o + dT(x,y)) / (T_o + dT(0,0))
+ *
+ *  we avoid directly fitting these values as the process would be a non-linear
+ *  least-squares problem for every pixel in the image, and thus very time
+ *  consuming.  There are linear options which may be used instead.
+ *  First, as T_o goes to a large value, s() approaches the value of f'(x,y).
+ *  Next, as T_o goes to a very small value, s() approaches the value of
+ *  f'(x,y)*dT(x,y)/dT(0,0).  Finally, when s() has the value of
+ *  f'(x,y)*(1 + dT(x,y)/dT(0,0))/2, T_o has the value of dT(0,0).  with data
+ *  points covering a reasonable dynamic range, we can solve for these three
+ *  values by interpolation and/or extrapolation.
+ *
+ *  To take the strategy one step further, we could use the above recipe to
+ *  obtain a guess for the three parameters and then apply non-linear fitting to
+ *  solve more accurately for the parameters.  If we limit this operation to a
+ *  handful of positions in the image (user defined, but the obvious choice would
+ *  be positions near the center, edges, and corners), then we may determine a
+ *  good value for dT(0,0).  Since there is only one dT(0,0) for the image, we
+ *  can apply the resulting measurement to the rest of the pixels in the image.
+ *  If dT(0,0) is not a free parameter, then the fitting process is linear in
+ *  terms of dT(x,y) and f'(x,y)
+ */
+
+/// Shutter correction parameters, applicable for a single pixel
+typedef struct
+{
+    double scale;                       ///< The normalisation for an exposure, A(k)
+    double offset;                      ///< The time offset, dTk
+    double offref;                      ///< The reference time offset, dTo
+}
+pmShutterCorrection;
+
+/// Allocator for shutter correction parameters
+pmShutterCorrection *pmShutterCorrectionAlloc();
+
+/// Guess a shutter correction, based on plot of counts vs exposure time
+///
+/// This function is used before doing the full non-linear fit, to get parameters close to the true.  Assumes
+/// exptime vector is sorted (ascending order; longest is last) prior to input.
+pmShutterCorrection *pmShutterCorrectionGuess(const psVector *exptime, ///< Exposure times for each exposure
+        const psVector *counts ///< Counts for each exposure
+                                             );
+
+/// Generate shutter correction based on a linear fit
+///
+/// Performs a linear fit to counts as a function of exposure time, with the reference time offset fixed (so
+/// that the system is linear).  Performs iterative clipping, if nIter > 1.
+pmShutterCorrection *pmShutterCorrectionLinFit(const psVector *exptime, ///< Exposure times for each exposure
+        const psVector *counts, ///< Counts for each exposure
+        const psVector *cntError, ///< Error in the counts
+        const psVector *mask, ///< Mask for each exposure
+        float offref, ///< Reference time offset
+        int nIter, ///< Number of iterations
+        float rej, ///< Rejection threshold (sigma)
+        psMaskType maskVal ///< Mask value
+                                              );
+
+/// Generate shutter correction based on a full non-linear fit
+///
+/// Performs a full non-linear fit to counts as a function of exposure time.  The main purpose is to solve for
+/// the reference time offset, so that future fits may be performed using linear fitting with the reference
+/// time offset fixed.
+pmShutterCorrection *pmShutterCorrectionFullFit(const psVector *exptime, ///< Exposure times for each exposure
+        const psVector *counts, ///< Counts for each exposure
+        const psVector *cntError, ///< Error in the counts
+        const pmShutterCorrection *guess ///< Initial guess
+                                               );
+
+/// Measure a shutter correction image from an array of images
+///
+/// Given an array of readouts (with known exposure times from the cell concepts), this function measures the
+/// shutter correction (our principal concern is for the time offset, rather than the normalisation) by
+/// measuring the reference time offset using the full non-linear fit for a small number of representative
+/// regions (middle and corners), and then using that to perform a linear fit to each pixel.
+bool pmShutterCorrectionMeasure(pmReadout *output, ///< Output readout
+                                const psArray *readouts, ///< Array of readouts
+                                int size, ///< Size of samples for statistics for non-linear fit
+                                psStatsOptions meanStat, ///< Statistic to use for mean
+                                psStatsOptions stdevStat, ///< Statistic to use for stdev
+                                int nIter, ///< Number of iterations
+                                float rej, ///< Rejection threshold (sigma)
+                                psMaskType maskVal ///< Mask value
+    );
+
+/// Apply a shutter correction
+///
+/// Given a shutter correction (with dT for each pixel), applies this correction to an input image.
+bool pmShutterCorrectionApply(pmReadout *readout, ///< Readout to which to apply shutter correction
+                              const pmReadout *shutter ///< Shutter correction readout, with dT for each pixel
+                             );
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Functions for doing the shutter correction piece-meal (don't have to read entire image stack into memory at
+// once).  A single read run through the stack is required, calling pmShutterCorrectionAddReadout on each.
+// Then pmShutterCorrectionReference provides the required reference shutter time, so that
+// pmShutterCorrectionGenerate can generate a shutter correction piece by piece as overlapping pixels from
+// each input are read in.
+
+
+/// Data for measuring the shutter correction
+typedef struct {
+    int num;                            ///< Number of images
+    int numCols, numRows;               ///< Size of images
+    psArray *regions;                   ///< Regions at which to measure statistics
+    psArray *mean;                      ///< Vector of means at each region
+    psArray *stdev;                     ///< Vector of standard deviations at each region
+    psVector *exptimes;                 ///< Exposure times for each image
+    psVector *refs;                     ///< Reference fluxes
+} pmShutterCorrectionData;
+
+/// Allocator for pmShutterCorrectionData
+pmShutterCorrectionData *pmShutterCorrectionDataAlloc(int numCols, int numRows, ///< Size of images
+                                                      int size ///< Size of regions
+    );
+
+/// Add a readout to the correction data
+///
+/// Performs statistics on the readout, recording the data
+bool pmShutterCorrectionAddReadout(pmShutterCorrectionData *data, ///< Correction data
+                                   const pmReadout *readout, ///< Readout to add
+                                   psStatsOptions meanStat, ///< Statistic to use for mean
+                                   psStatsOptions stdevStat, ///< Statistic to use for stdev
+                                   psMaskType maskVal, ///< Mask value
+                                   psRandom *rng ///< Random number generator
+    );
+
+/// Calculate the reference shutter time from the correction data
+float pmShutterCorrectionReference(const pmShutterCorrectionData *data ///< Correction data
+    );
+
+/// Generate a shutter correction
+///
+/// Performs the linear fit to each pixel in the stack.
+bool pmShutterCorrectionGenerate(pmReadout *shutter, ///< Shutter correction
+                                 pmReadout *pattern, ///< Background pattern (or NULL)
+                                 const psArray *inputs, ///< Stack of input pmReadouts
+                                 float reference, ///< Reference shutter time (from pmShutterCorrectionRef)
+                                 const pmShutterCorrectionData *data, ///< Correction data
+                                 int nIter, ///< Number of iterations
+                                 float rej, ///< Rejection threshold (sigma)
+                                 psMaskType maskVal ///< Mask value
+    );
+
+
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmSkySubtract.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmSkySubtract.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmSkySubtract.c	(revision 15358)
@@ -0,0 +1,738 @@
+/** @file  pmSubtractSky.c
+ *
+ *  This file will contain a module which will create a model of the
+ *  background sky and subtract that from the input image.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-04-04 22:42:48 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ *
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmSubtractSky.h"
+
+// XXX: Get rid of the.  Create pmUtils.h
+psImage *p_psDetermineTrimmedImage(
+    pmReadout *in
+);
+
+/******************************************************************************
+DetermineNumBits(data): This routine takes an enum psStatsOptions as an
+argument and returns the number of non-zero bits.
+
+XXX: This code is duplicated in the ReadoutCombine file.
+ *****************************************************************************/
+static psS32 DetermineNumBits(psStatsOptions data)
+{
+    psTrace("psModules.detrend", 4, "Calling DetermineNumBits(0x%x)\n", data);
+
+    psS32 i;
+    psU64 tmpData = data;
+    psS32 numBits = 0;
+
+    for (i=0;i<(8 * sizeof(psStatsOptions));i++) {
+        if (0x0001 & tmpData) {
+            numBits++;
+        }
+        tmpData = tmpData >> 1;
+    }
+
+    psTrace("psModules.detrend", 4,
+            "Calling DetermineNumBits(0x%x) -> %d\n", data, numBits);
+    return(numBits);
+}
+
+/******************************************************************************
+getHighestPriorityStatOption(statOptions): this routine takes as input a
+psStats->options with multiple options set and returns one with a single
+option set according to the precedence set in the SDRS.
+ *****************************************************************************/
+static psU64 getHighestPriorityStatOption(psU64 statOptions)
+{
+    psTrace("psModules.detrend", 4,
+            "Calling getHighestPriorityStatOption(0x%x)\n", statOptions);
+
+    if (statOptions & PS_STAT_SAMPLE_MEAN) {
+        return(PS_STAT_SAMPLE_MEAN);
+    } else if (statOptions & PS_STAT_SAMPLE_MEDIAN) {
+        return(PS_STAT_SAMPLE_MEDIAN);
+    } else if (statOptions & PS_STAT_CLIPPED_MEAN) {
+        return(PS_STAT_CLIPPED_MEAN);
+    } else if (statOptions & PS_STAT_FITTED_MEAN) {
+        return(PS_STAT_FITTED_MEAN);
+    } else if (statOptions & PS_STAT_ROBUST_MEDIAN) {
+        return(PS_STAT_ROBUST_MEDIAN);
+    }
+    psError(PS_ERR_UNKNOWN, true, "Unallowable option requested for statistically binning image pixels.\n");
+    return(-1);
+    // XXX
+    //else if (statOptions & PS_STAT_ROBUST_MODE) {
+    //    return(PS_STAT_ROBUST_MODE);
+    //}
+}
+
+/******************************************************************************
+psImage *binImage(origImage, binFactor, statOptions): This routine takes an
+input psImage and scales it smaller by a factor of binFactor.  The statistic
+used in combining input pixels is specified in statOptions.
+
+XXX: use static vectors for myStats, binVector and binMask.
+XXX: I coded this before I was aware of a psLib reBin function.  I don't
+use this function in this module.  I'm keeping it here in the event that
+requirements change and we might need a custom reBin function.
+ *****************************************************************************/
+#if 0
+static psImage *binImage(psImage *origImage,
+                         int binFactor,
+                         psStatsOptions statOptions)
+{
+    psTrace("psModules.detrend", 4, "Calling binImage(%d)\n", binFactor);
+
+    if (binFactor <= 0) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: binImage(): binFactor is %d\n", binFactor);
+        return(origImage);
+    }
+    if (binFactor == 1) {
+        return(origImage);
+    }
+
+    psVector *binVector = psVectorAlloc(binFactor * binFactor, PS_TYPE_F32);
+    psVector *binMask = psVectorAlloc(binFactor * binFactor, PS_TYPE_U8);
+    psStats *myStats = psStatsAlloc(statOptions);
+
+    for (psS32 row = 0; row < origImage->numRows ; row+=binFactor) {
+        for (psS32 col = 0; col < origImage->numCols ; col+=binFactor) {
+            psS32 count = 0;
+            for (psS32 binRow = 0; binRow <= binFactor ; binRow++) {
+                for (psS32 binCol = 0; binCol <= binFactor ; binCol++) {
+                    if (((row + binRow) < origImage->numRows) &&
+                            ((col + binCol) < origImage->numCols)) {
+                        binVector->data.F32[count] =
+                            origImage->data.F32[row + binRow][col + binCol];
+                        binMask->data.U8[count] = 0;
+                    } else {
+                        binVector->data.F32[count] = 0.0;
+                        binMask->data.U8[count] = 1;
+                    }
+                    count++;
+                }
+            }
+            psStats *rc1 = psVectorStats(myStats, binVector, NULL, binMask, 1);
+            if (rc1 == NULL) {
+                psError(PS_ERR_UNKNOWN, false, "psVectorStats(): could not perform requested statistical operation.  Returning in image.\n");
+                return(origImage);
+            }
+            psF64 statValue;
+            psBool rc = p_psGetStatValue(rc1, &statValue);
+
+            if (rc == true) {
+                origImage->data.F32[row][col] = (psF32) statValue;
+            } else {
+                origImage->data.F32[row][col] = 0.0;
+                psLogMsg(__func__, PS_LOG_WARN,
+                         "WARNING: pmSubtractSky(), binImage(): p_psGetStatValue() was FALSE\n");
+            }
+        }
+    }
+    psFree(binVector);
+    psFree(binMask);
+    psFree(myStats);
+
+    psTrace("psModules.detrend", 4, "Exiting binImage(%d)\n", binFactor);
+    return(origImage);
+}
+#endif
+
+/******************************************************************************
+CalculatePolyTerms(xOrder, yOrder): this routine will calculate the number of
+coefficients (or terms) in a 2-D polynomial of order (xOrder, yOrder).
+
+XXX: Use your brain and figure out the analytical expression.
+
+XXX: Why isn't it simply (xOrder+1) * (yOrder+1)?
+ *****************************************************************************/
+static psS32 CalculatePolyTerms(psS32 xOrder, psS32 yOrder)
+{
+    psTrace("psModules.detrend", 4,
+            "Calling CalculatePolyTerms(%d, %d)\n", xOrder, yOrder);
+
+    psS32 maxOrder = PS_MAX(xOrder, yOrder);
+    psS32 localPolyTerms = 0;
+    psS32 order = 0;
+    psS32 num=0;
+
+    for (order=0;order<=maxOrder;order++) {
+        for (num=0;num<=order;num++) {
+            if (((order-num) <= xOrder) && (num <= yOrder)) {
+                localPolyTerms++;
+            }
+        }
+    }
+    psTrace("psModules.detrend", 4,
+            "Exiting CalculatePolyTerms(%d, %d) -> %d\n", xOrder, yOrder, localPolyTerms);
+    return(localPolyTerms);
+
+    //    return((xOrder+1) * (yOrder+1));
+}
+
+/******************************************************************************
+buildPolyTerms(): this routine computes a 2-D array polyTerms[][] that holds
+terms for the polynomial that is used to model the sky background.  We use
+this array primarily for convenience in computations involving sky model
+polynomials.  It is defined as:
+    polyTerms[i][0] = the power to which X is raised in the i-th term of in an
+    poly-order sky background polynomial.
+
+    polyTerms[i][1] = the power to which Y is raised in the i-th term of in an
+    poly-order sky background polynomial.
+ *****************************************************************************/
+static psS32 **buildPolyTerms(psS32 xOrder, psS32 yOrder)
+{
+    psTrace("psModules.detrend", 4,
+            "Calling buildPolyTerms(%d, %d)\n", xOrder, yOrder);
+
+    psS32 i=0;
+    psS32 order = 0;
+    psS32 num=0;
+    psS32 localPolyTerms = CalculatePolyTerms(xOrder, yOrder);
+    psS32 maxOrder = PS_MAX(xOrder, yOrder);
+
+    // Create the data structure which we hold the xy order of each coeff.
+    psS32 **polyTerms = (psS32 **) psAlloc(localPolyTerms * sizeof(psS32 *));
+    for (i=0; i < localPolyTerms ; i++) {
+        polyTerms[i] = (psS32 *) psAlloc(2 * sizeof(psS32));
+    }
+
+    i=0;
+    // This code segment loops through each term i in the polynomial and
+    // calculates the power to which x/y are raised in that i-th term.
+    // We first do the 0-order terms, then the 1-order terms, etc.
+    for (order=0;order<=maxOrder;order++) {
+        for (num=0;num<=order;num++) {
+            if (((order-num) <= xOrder) && (num <= yOrder)) {
+                polyTerms[i][0] = order-num;
+                polyTerms[i][1] = num;
+                i++;
+            }
+        }
+    }
+
+    if (psTraceGetLevel("psModules.detrend") >= 10) {
+        for (i=0; i < localPolyTerms ; i++) {
+            printf("x^%d * y^%d\n", polyTerms[i][0], polyTerms[i][1]);
+        }
+    }
+
+    psTrace("psModules.detrend", 4,
+            "Exiting buildPolyTerms(%d, %d)\n", xOrder, yOrder);
+    return(polyTerms);
+}
+
+/******************************************************************************
+This procedure calculates various combinations of powers of x and y and stores
+them in the data structure p_psPolySums[][].  After it completes:
+
+    p_psPolySums[i][j] == x^i * y^j
+
+XXX: Use a psImage for the p_psPolySums data structure?
+XXX: p_psPolySums: should this be a global?  Did you get the storage classifier
+     and name correct?
+XXX: Use variable size arrays for p_psPolySums[][].
+XXX: Must initialize p_psPolySums[][]?
+ *****************************************************************************/
+#define PS_MAX_POLYNOMIAL_ORDER 20
+
+psF64 p_psPolySums[PS_MAX_POLYNOMIAL_ORDER+1][PS_MAX_POLYNOMIAL_ORDER+1];
+static void buildSums(psF64 x,
+                      psF64 y,
+                      psS32 xOrder,
+                      psS32 yOrder)
+{
+    psTrace("psModules.detrend", 4,
+            "Calling buildPolyTerms(%d, %d)\n", xOrder, yOrder);
+
+    psS32 i = 0;
+    psS32 j = 0;
+    psF64 xSum = 0.0;
+    psF64 ySum = 0.0;
+
+    xSum = 1.0;
+    ySum = 1.0;
+    for(i=0;i<=xOrder;i++) {
+        ySum = xSum;
+        for(j=0;j<=yOrder;j++) {
+            p_psPolySums[i][j] = ySum;
+            ySum*= y;
+        }
+        xSum*= x;
+    }
+    psTrace("psModules.detrend", 4,
+            "Exiting buildPolyTerms(%d, %d)\n", xOrder, yOrder);
+}
+
+/******************************************************************************
+ImageFitPolynomial(myPoly, dataImage, maskImage): this private routine takes
+an input image along with a mask and fits a polynomial to it.  The degree of
+the polynomial is specified by input parameter myPoly, and need not be
+symmetrical in orders of X and Y.  The polynomial must be type
+PS_POLYNOMIAL_ORD.  If there are not enough rows or columns in the input image
+for the order of the polynomial, then that order is reduced.  The algorithm
+used in this routine is based on that of the pilot project ADD, but is not
+documented anywhere.
+
+XXX: Different trace message facilities in use here.
+ *****************************************************************************/
+static psPolynomial2D *ImageFitPolynomial(
+    psPolynomial2D *myPoly,
+    psImage *dataImage,
+    psImage *maskImage)
+{
+    psTrace("psModules.detrend", 4,
+            "Calling ImageFitPolynomial()\n");
+    PS_ASSERT_POLY_NON_NULL(myPoly, NULL);
+    PS_ASSERT_POLY_TYPE(myPoly, PS_POLYNOMIAL_ORD, NULL);
+    PS_ASSERT_IMAGE_NON_NULL(dataImage, NULL);
+    PS_ASSERT_IMAGE_NON_EMPTY(dataImage, NULL);
+    PS_ASSERT_IMAGE_TYPE(dataImage, PS_TYPE_F32, NULL);
+    PS_ASSERT_IMAGE_NON_NULL(maskImage, NULL);
+    PS_ASSERT_IMAGE_NON_EMPTY(maskImage, NULL);
+    PS_ASSERT_IMAGE_TYPE(maskImage, PS_TYPE_U8, NULL);
+    PS_ASSERT_IMAGES_SIZE_EQUAL(dataImage, maskImage, NULL);
+    psS32 oldPolyX = -1;
+    psS32 oldPolyY = -1;
+
+    // The matrix equations become singular if there are more powers of X
+    // in myPoly then there are rows of the image.  I think.  Similarly for
+    // powers of Y and columns.  So.  Here we reduce the complexity of the
+    // polynomial if there are not enough rows/columns in the input image.
+
+    if ((myPoly->nX + 1) > dataImage->numRows) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: ImageFitPolynomial(): Reducing polynomial complexity in x-dimension.\n");
+        oldPolyX = myPoly->nX;
+        myPoly->nX = dataImage->numRows - 1;
+    }
+    if ((myPoly->nY + 1) > dataImage->numCols) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: ImageFitPolynomial(): Reducing polynomial complexity in y-dimension.\n");
+        oldPolyY = myPoly->nY;
+        myPoly->nY = dataImage->numCols - 1;
+    }
+    psS32 i;
+    psS32 j;
+    psS32 x;
+    psS32 y;
+    psS32 aRow;
+    psS32 aCol;
+    psS32 **polyTerms = buildPolyTerms(myPoly->nX, myPoly->nY);
+    // We determine how many coefficients will be in the polynomial that we
+    // are fitting to this image.
+    psS32 localPolyTerms = CalculatePolyTerms(myPoly->nX, myPoly->nY);
+    psImage *A = psImageAlloc(localPolyTerms, localPolyTerms, PS_TYPE_F64);
+    psImage *Aout = psImageAlloc(localPolyTerms, localPolyTerms, PS_TYPE_F64);
+    psVector *B = psVectorAlloc(localPolyTerms, PS_TYPE_F64);
+    psVector *outPerm = NULL;
+
+    //
+    // Initialize A matrix and B vector.
+    //
+    psImageInit(A, 0.0);
+    psVectorInit(B, 0.0);
+
+    //
+    // We build the A matrix and B vector.
+    //
+    for (x=0;x<dataImage->numRows;x++) {
+        for (y=0;y<dataImage->numCols;y++) {
+            if (maskImage->data.U8[x][y] == 0) {
+                buildSums((psF64) x, (psF64) y, myPoly->nX, myPoly->nY);
+
+                /************************************************************
+                This code dervies from equation (7) of the pilot ADD.  However,
+                it is not exactly the same in that the order of the polynomial
+                may be different in X And Y.
+
+                Equation (7) from the pilot ADD describes 16 linear equations.
+                The i-th equation is simply the partial derivative of the
+                sky background polynomial (1) w.r.t. to the i-th term in
+                that polynomial.  The i-th equation is stored in row i of
+                matrix A[][] (matrix A[][] has origin (1,1), not (0,0)).  To
+                compute A[i][j] we simply multiply the j-th term of the Sky
+                Background Polynomial (SBP) by the i-th term of SBP.
+                ************************************************************/
+                for (aRow=0;aRow<localPolyTerms;aRow++) {
+                    for (aCol=0;aCol<localPolyTerms;aCol++) {
+                        A->data.F64[aRow][aCol]+=
+                            (p_psPolySums[ polyTerms[aCol][0] ][ polyTerms[aCol][1] ] *
+                             p_psPolySums[ polyTerms[aRow][0] ][ polyTerms[aRow][1] ]);
+                    }
+                }
+                // Build the B[] vector, which is the right-hand side of (7).
+                for (i=0;i<localPolyTerms;i++) {
+                    B->data.F64[i]+= dataImage->data.F32[x][y] *
+                                     p_psPolySums[ polyTerms[i][0] ][ polyTerms[i][1] ];
+                }
+            }
+        }
+    }
+
+    if (psTraceGetLevel(".psModule.pmSubtractSky.ImageFitPolynomial") >= 8) {
+        for (aRow=0;aRow<localPolyTerms;aRow++) {
+            for (aCol=0;aCol<localPolyTerms;aCol++) {
+                printf("A[%d][%d] is %f\n", aRow, aCol,
+                       A->data.F64[aRow][aCol]);
+            }
+        }
+
+        for (i=0;i<=localPolyTerms;i++) {
+            printf("B[%d] is %f\n", i, B->data.F64[i]);
+        }
+    }
+
+    //
+    // Solve the matrix equations for the polynomial coefficients C.
+    // XXX: How do we know if these matrix operations were successful?
+    //
+    Aout = psMatrixLUD(Aout, &outPerm, A);
+    PS_ASSERT_IMAGE_NON_NULL(Aout, NULL);
+    PS_ASSERT_IMAGE_NON_EMPTY(Aout, NULL);
+    psVector *C = psVectorAlloc(localPolyTerms, PS_TYPE_F64);
+    psMatrixLUSolve(C, Aout, B, outPerm);
+
+    //
+    // Set the appropriate coefficients in the myPoly structure.
+    //
+    for (i=0;i<localPolyTerms;i++) {
+        myPoly->coeff[ polyTerms[i][0] ][ polyTerms[i][1] ] = C->data.F64[i];
+        psTrace("psModules.detrend", 6,
+                "myPoly->coeff[%d][%d] is %f\n", polyTerms[i][0], polyTerms[i][1], myPoly->coeff[ polyTerms[i][0] ][ polyTerms[i][1] ]);
+    }
+
+    //
+    // Free data structures that were allocated in this module.
+    //
+    for (i=0;i<localPolyTerms;i++) {
+        psFree(polyTerms[i]);
+    }
+    psFree(polyTerms);
+    psFree(A);
+    psFree(Aout);
+    psFree(B);
+    psFree(C);
+    psFree(outPerm);
+
+    //
+    // We restore the original size of the polynomial and set remaining
+    // coefficients to 0.0, if necessary.
+    //
+    // XXX: Verify this works after poly nOrder/nTerm change.
+    //
+    if (oldPolyX != -1) {
+        myPoly->nX = oldPolyX;
+        for (i=oldPolyX ; i < (1 + myPoly->nX) ; i++) {
+            for (j=0;j<(1 + myPoly->nY) ; j++) {
+                myPoly->coeff[i][j] = 0.0;
+            }
+        }
+    }
+    if (oldPolyY != -1) {
+        myPoly->nY = oldPolyY;
+        for (i=0 ; i < (1 + myPoly->nX) ; i++) {
+            for (j=oldPolyY;j < (1 + myPoly->nY) ; j++) {
+                myPoly->coeff[i][j] = 0.0;
+            }
+        }
+    }
+
+    psTrace("psModules.detrend", 4,
+            "Exiting ImageFitPolynomial()\n");
+    //    psTrace("psModules.detrend", 4,
+    //            "---- ImageFitPolynomial() end successfully ----\n");
+    return(myPoly);
+}
+
+
+/******************************************************************************
+pmReadout pmSubtractSky():
+
+XXX: use static vectors for myStats, and the binned image
+
+XXX: The SDR is silent about types.  PS_TYPE_F32 is implemented here.
+
+XXX: Sync the psTrace message facilities.
+ *****************************************************************************/
+pmReadout *pmSubtractSky(pmReadout *in,
+                         void *fitSpec,
+                         psFit fit,
+                         psS32 binFactor,
+                         psStats *stats,
+                         psF32 clipSD)
+{
+    PS_ASSERT_READOUT_NON_NULL(in, NULL);
+    PS_ASSERT_READOUT_NON_EMPTY(in, NULL);
+    PS_ASSERT_READOUT_TYPE(in, PS_TYPE_F32, NULL);
+    PS_WARN_PTR_NON_NULL(in->parent);
+    if (in->parent != NULL) {
+        PS_WARN_PTR_NON_NULL(in->parent->concepts);
+    }
+    psTrace("psModules.detrend", 4,
+            "---- pmSubtractSky() begin ----\n");
+
+    if ((fit != PM_FIT_NONE) &&
+            (fit != PM_FIT_POLYNOMIAL) &&
+            (fit != PM_FIT_SPLINE)) {
+        psError(PS_ERR_UNKNOWN, true, "psFit is unallowable (%d).  Returning in image.\n", fit);
+        return(in);
+    }
+
+    psStatsOptions statOptions = 0;
+
+    //
+    // Return the original input readout if the fit specs are poorly defined.
+    // No warning or error messages should be generated.
+    //
+    if ((fitSpec == NULL) ||
+            ((fit == PM_FIT_NONE) || (fit == PM_FIT_SPLINE))) {
+        //        psLogMsg(__func__, PS_LOG_WARN, "Fit specs are poorly defined.  Returning in image.\n");
+        return(in);
+    }
+
+    //
+    // Determine trimmed image from metadata.
+    //
+
+    psImage *trimmedImg = p_psDetermineTrimmedImage(in);
+    psImage *binnedImage = NULL;
+    psPolynomial2D *myPoly = NULL;
+    psImage *binnedMaskImage = NULL;
+    psU32 oldStatOptions = 0;
+
+    //
+    // Determine which statistic to use when binning pixels, if any.
+    //
+    if (stats != NULL) {
+        statOptions = stats->options;
+        if (1 < DetermineNumBits(statOptions)) {
+            psLogMsg(__func__, PS_LOG_WARN, "WARNING: Multiple statistical options have been requested.\n");
+            statOptions = getHighestPriorityStatOption(statOptions);
+            if (statOptions == -1) {
+                psError(PS_ERR_UNKNOWN, true, "Not allowable stats->option was specified.  Returning in image.\n");
+                return(in);
+            }
+            // Save old input "stats" parameter.
+            oldStatOptions = stats->options;
+            stats->options = statOptions;
+        }
+        if (0 == DetermineNumBits(statOptions)) {
+            psLogMsg(__func__, PS_LOG_WARN,
+                     "WARNING: pmSubtractSky(): no stats->options was requested\n");
+        }
+    }
+
+    //
+    // Generate required warning messages.
+    //
+    if (binFactor <= 0) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: pmSubtractSky(): binFactor is %d\n", binFactor);
+    }
+    if (stats == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: pmSubtractSky(): input parameter stats is NULL\n");
+    }
+
+    //
+    // Bin the input image according to input parameters.
+    // Create a new binned image mask.
+    //
+    if ((binFactor <= 1) || (stats == NULL) || (0 == DetermineNumBits(statOptions))) {
+        // No binning is required here.  Simply create a copy of the image
+        // and a mask.
+        binnedImage = psImageCopy(binnedImage, trimmedImg, PS_TYPE_F32);
+        if (binnedImage == NULL) {
+            psError(PS_ERR_UNKNOWN, false, "psImageCopy() returned NULL.  Returning in image.\n");
+            return(in);
+        }
+
+        if (in->mask != NULL) {
+            binnedMaskImage = psImageCopy(binnedMaskImage, in->mask, PS_TYPE_U8);
+            if (binnedMaskImage == NULL) {
+                psError(PS_ERR_UNKNOWN, false, "psImageCopy() returned NULL.  Returning in image.\n");
+                psFree(binnedImage);
+                return(in);
+            }
+        } else {
+            binnedMaskImage = psImageAlloc(binnedImage->numCols,
+                                           binnedImage->numRows,
+                                           PS_TYPE_U8);
+            psImageInit(binnedMaskImage, 0);
+        }
+    } else {
+        binnedImage = psImageRebin(NULL, trimmedImg, in->mask, 0, binFactor, stats);
+        if (binnedImage == NULL) {
+            psError(PS_ERR_UNKNOWN, false, "psImageRebin() returned NULL.  Returning in image.\n");
+            return(in);
+        }
+        binnedMaskImage = psImageAlloc(binnedImage->numCols,
+                                       binnedImage->numRows,
+                                       PS_TYPE_U8);
+        psImageInit(binnedMaskImage, 0);
+    }
+    psTrace("psModules.detrend", 4,
+            "binnedImage size is (%d, %d)\n", binnedImage->numRows, binnedImage->numCols);
+
+    //
+    // Clip pixels that are outside the acceptable range.
+    //
+    if (clipSD <= 0.0) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: pmSubtractSky(): clipSD is %f\n", clipSD);
+    } else {
+        // Determine the mean and standard deviation of the binned image.
+        psStats *myStats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+        if (!psImageStats(myStats, binnedImage, NULL, 0)) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Couldn't get statistics for image.\n");
+            return NULL;
+        }
+        psF64 binnedMean = myStats->sampleMean;
+        psF64 binnedStdev = myStats->sampleStdev;
+        psFree(myStats);
+        psTrace("psModules.detrend", 6,
+                "binned StDev is %f\n", binnedStdev);
+
+        // Clip all pixels which are more than clipSD sigmas from the mean.
+        psTrace("psModules.detrend", 6,
+                "clipSD is %f\n", clipSD);
+
+        for (psS32 row = 0; row < binnedImage->numRows ; row++) {
+            for (psS32 col = 0; col < binnedImage->numCols ; col++) {
+                if (fabs(binnedImage->data.F32[row][col] - binnedMean) >
+                        (clipSD * binnedStdev)) {
+                    binnedMaskImage->data.U8[row][col] = 1;
+                }
+            }
+        }
+    }
+
+    //
+    // Fit the polynomial to the binned image
+    //
+    if (fit == PM_FIT_POLYNOMIAL) {
+        myPoly = (psPolynomial2D *) fitSpec;
+        PS_ASSERT_POLY_NON_NULL(myPoly, NULL);
+        PS_ASSERT_POLY_TYPE(myPoly, PS_POLYNOMIAL_ORD, NULL);
+
+        myPoly = ImageFitPolynomial(myPoly, binnedImage, binnedMaskImage);
+
+        if (myPoly != NULL) {
+            // Set the pixels in the binned image to that of the polynomial.
+            binnedImage = psImageEvalPolynomial(binnedImage, myPoly);
+            if (binnedImage == NULL) {
+                psError(PS_ERR_UNKNOWN, false, "psImageEvalPolynomial() returned NULL.  Returning in image.\n");
+                psFree(binnedMaskImage);
+                if (!((binFactor <= 1) || (stats == NULL))) {
+                    psFree(binnedImage);
+                }
+                if (oldStatOptions != 0) {
+                    stats->options = statOptions;
+                }
+                return(in);
+            }
+        } else {
+            psLogMsg(__func__, PS_LOG_WARN,
+                     "WARNING: pmSubtractSky(): could not model sky with a polynomial.\n");
+            psFree(binnedMaskImage);
+            if (!((binFactor <= 1) || (stats == NULL))) {
+                psFree(binnedImage);
+            }
+            if (oldStatOptions != 0) {
+                stats->options = statOptions;
+            }
+            return(in);
+        }
+    } else {
+        // We shouldn't get here since we check this above.
+        psError(PS_ERR_UNKNOWN, true, "Unallowable fit type.  Returning in image.\n");
+        psFree(binnedMaskImage);
+        if (!((binFactor <= 1) || (stats == NULL))) {
+            psFree(binnedImage);
+        }
+        if (oldStatOptions != 0) {
+            stats->options = statOptions;
+        }
+        return(in);
+    }
+
+    //
+    //Subtract the polynomially fitted image from the original image
+    //
+    if (binFactor <= 1) {
+        // The binned image is the same size as the original image.
+        for (psS32 row = 0; row < trimmedImg->numRows ; row++) {
+            for (psS32 col = 0; col < trimmedImg->numCols ; col++) {
+                trimmedImg->data.F32[row][col]-= binnedImage->data.F32[row][col];
+            }
+        }
+    } else {
+
+        psImageInterpolateOptions *interp = psImageInterpolateOptionsAlloc(PS_INTERPOLATE_BILINEAR,
+                                                                           binnedImage, NULL, NULL, 0,
+                                                                           0.0, 0.0, 0, 0, 0.0);
+
+        for (psS32 row = 0; row < trimmedImg->numRows ; row++) {
+            for (psS32 col = 0; col < trimmedImg->numCols ; col++) {
+                // We calculate the F32 value of the pixel coordinates in the
+                // binned image and then use a pixel interpolation routine to
+                // determine the value of the pixel at that location.
+                psF32 binRowF64 = ((psF32) row) / ((psF32) binFactor);
+                psF32 binColF64 = ((psF32) col) / ((psF32) binFactor);
+
+                // We add 0.5 to the pixel locations since the pixel
+                // interpolation routine defines the location of pixel
+                // (i, j) as (i+0.5, j+0.5).
+                binRowF64+= 0.5;
+                binColF64+= 0.5;
+
+                double binPixel;
+                if (!psImagePixelInterpolate(&binPixel, NULL, NULL, binColF64, binRowF64, interp)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to interpolate image.");
+                    psFree(interp);
+                    psFree(binnedImage);
+                    return NULL;
+                }
+                trimmedImg->data.F32[row][col] -= binPixel;
+
+                psTrace("psModules.detrend", 8,
+                        "image[%d][%d] <--> binnedImage[%.2f][%.2f]: %lf\n",
+                        row, col, binRowF64-0.5, binColF64-0.5, binPixel);
+            }
+        }
+        psFree(interp);
+
+    }
+    psFree(binnedMaskImage);
+    psFree(binnedImage);
+    if (oldStatOptions != 0) {
+        stats->options = statOptions;
+    }
+
+    psTrace("psModules.detrend", 4,
+            "---- pmSubtractSky() exit successfully ----\n");
+    return(in);
+}
Index: /branches/eam_branch_20071023/psModules/src/detrend/pmSkySubtract.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/detrend/pmSkySubtract.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/detrend/pmSkySubtract.h	(revision 15358)
@@ -0,0 +1,34 @@
+/* @file  pmSubtractSky.h
+ *
+ * This file will contain a module which will create a model of the
+ * background sky and subtract that from the input image.
+ *
+ * @author GLG, MHPCC
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#ifndef PM_SUBTRACT_SKY_H
+#define PM_SUBTRACT_SKY_H
+
+/// @addtogroup detrend Detrend Creation and Application
+/// @{
+
+// XXX: this is pmFit in pmSubtractBias.c, named psFit here.
+typedef enum {
+    PM_FIT_NONE,                              ///< No fit
+    PM_FIT_POLYNOMIAL,                        ///< Fit polynomial
+    PM_FIT_SPLINE                             ///< Fit cubic splines
+} psFit;
+
+pmReadout *pmSubtractSky(pmReadout *in,
+                         void *fitSpec,
+                         psFit fit,
+                         int binFactor,
+                         psStats *stats,
+                         float clipSD);
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/extras/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/src/extras/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/extras/.cvsignore	(revision 15358)
@@ -0,0 +1,9 @@
+.deps
+.libs
+Makefile
+Makefile.in
+libpsmodulesextras.la
+libpsmodulesextras_la-pmKapaPlots.lo
+libpsmodulesextras_la-psIOBuffer.lo
+libpsmodulesextras_la-psPipe.lo
+libpsmodulesextras_la-psVectorBracket.lo
Index: /branches/eam_branch_20071023/psModules/src/extras/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/src/extras/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/extras/Makefile.am	(revision 15358)
@@ -0,0 +1,17 @@
+noinst_LTLIBRARIES = libpsmodulesextras.la
+
+libpsmodulesextras_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS) -I../pslib/
+libpsmodulesextras_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulesextras_la_SOURCES  = \
+	psVectorBracket.c \
+	psPipe.c \
+	psIOBuffer.c \
+	pmKapaPlots.c
+
+pkginclude_HEADERS = \
+	psVectorBracket.h \
+	psPipe.h \
+	psIOBuffer.h \
+	pmKapaPlots.h
+
+CLEANFILES = *~
Index: /branches/eam_branch_20071023/psModules/src/extras/pmKapaPlots.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/extras/pmKapaPlots.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/extras/pmKapaPlots.c	(revision 15358)
@@ -0,0 +1,234 @@
+/** @file  pmAstrometryWCS.c
+ *
+ *  @brief functions to convert FITS WCS keywords to / from pmFPA structures
+ *
+ *  @ingroup Astrometry
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-06-20 02:20:07 $
+ *
+ *  Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+#include "pmKapaPlots.h"
+
+// the top portion of this file defines plotting functions which use kapa for plotting.
+// if kapa is not available, these functions are defined in the bottom portion as stubs
+// which perform NOP and return false (XXX should this be 'true' ??)
+
+# if (HAVE_KAPA)
+
+    // XXX not thread safe (perhaps not needed)
+    // to make this thread safe, we could check the thread ID and tie to it
+    static int kapa_fd = -1;
+
+int pmKapaOpen (bool showWindow)
+{
+    char kapa[64];
+
+    if (showWindow) {
+        strcpy (kapa, "kapa");
+    } else {
+        strcpy (kapa, "kapa -noX");
+    }
+
+    if (kapa_fd == -1) {
+        // kapa_fd = KapaOpen (kapa, "psphot");
+        kapa_fd = KapaOpenNamedSocket (kapa, "psphot");
+    }
+    return kapa_fd;
+}
+
+bool pmKapaClose ()
+{
+
+    if (kapa_fd == -1)
+        return true;
+    KapaClose (kapa_fd);
+    kapa_fd = -1;
+    return true;
+}
+
+bool pmKapaPlotVectorPair_AutoLimits_OpenGraph (int kapa, Graphdata *graphdata, psVector *xVec, psVector *yVec)
+{
+
+    // set limits based on data values
+    graphdata->xmin = +FLT_MAX;
+    graphdata->xmax = -FLT_MAX;
+    graphdata->ymin = +FLT_MAX;
+    graphdata->ymax = +FLT_MAX;
+    for (int i = 0; i < xVec->n; i++) {
+        graphdata->xmin = PS_MIN (graphdata->xmin, xVec->data.F32[i]);
+        graphdata->xmax = PS_MAX (graphdata->xmax, xVec->data.F32[i]);
+        graphdata->ymin = PS_MIN (graphdata->ymin, yVec->data.F32[i]);
+        graphdata->ymax = PS_MAX (graphdata->ymax, yVec->data.F32[i]);
+    }
+    // add 5% to range
+    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;
+
+    KapaSetLimits (kapa, graphdata);
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, graphdata);
+
+    KapaPrepPlot (kapa, xVec->n, graphdata);
+    KapaPlotVector (kapa, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa, yVec->n, yVec->data.F32, "y");
+    return true;
+}
+
+bool pmKapaPlotVectorPair (psVector *xVec, psVector *yVec)
+{
+
+    Graphdata graphdata;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    if (xVec->n != yVec->n)
+        return false;
+
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa);
+
+    // set limits based on data values
+    graphdata.xmin = +FLT_MAX;
+    graphdata.xmax = -FLT_MAX;
+    graphdata.ymin = +FLT_MAX;
+    graphdata.ymax = -FLT_MAX;
+    for (int i = 0; i < xVec->n; i++) {
+        graphdata.xmin = PS_MIN (graphdata.xmin, xVec->data.F32[i]);
+        graphdata.xmax = PS_MAX (graphdata.xmax, xVec->data.F32[i]);
+        graphdata.ymin = PS_MIN (graphdata.ymin, yVec->data.F32[i]);
+        graphdata.ymax = PS_MAX (graphdata.ymax, yVec->data.F32[i]);
+    }
+    // add 5% to range
+    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;
+
+    KapaSetLimits (kapa, &graphdata);
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, &graphdata);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, xVec->n, &graphdata);
+    KapaPlotVector (kapa, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa, yVec->n, yVec->data.F32, "y");
+
+    return true;
+}
+
+bool pmKapaPlotVectorTriple_AutoLimits_OpenGraph (int kapa, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing)
+{
+
+    // set limits based on data values
+    graphdata->xmin = +FLT_MAX;
+    graphdata->xmax = -FLT_MAX;
+    graphdata->ymin = +FLT_MAX;
+    graphdata->ymax = -FLT_MAX;
+    float zmin = +FLT_MAX;
+    float zmax = -FLT_MAX;
+    for (int i = 0; i < xVec->n; i++) {
+        graphdata->xmin = PS_MIN (graphdata->xmin, xVec->data.F32[i]);
+        graphdata->xmax = PS_MAX (graphdata->xmax, xVec->data.F32[i]);
+        graphdata->ymin = PS_MIN (graphdata->ymin, yVec->data.F32[i]);
+        graphdata->ymax = PS_MAX (graphdata->ymax, yVec->data.F32[i]);
+        zmin = PS_MIN (zmin, zVec->data.F32[i]);
+        zmax = PS_MAX (zmax, zVec->data.F32[i]);
+    }
+
+    // add 5% to range
+    float range;
+
+    psVector *zScale = psVectorAlloc (zVec->n, PS_DATA_F32);
+
+    range = zmax - zmin;
+    if (range == 0.0) {
+        psVectorInit (zScale, 1.0);
+    } else {
+        for (int i = 0; i < zVec->n; i++) {
+            if (increasing) {
+                zScale->data.F32[i] = PS_MIN (1.5, PS_MAX(0.05, 1.5*(zVec->data.F32[i] - zmin)/range));
+            } else {
+                zScale->data.F32[i] = PS_MIN (1.5, PS_MAX(0.05, 1.5*(zmax - zVec->data.F32[i])/range));
+            }
+        }
+    }
+
+    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;
+
+    KapaSetLimits (kapa, graphdata);
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, graphdata);
+
+    // the point size will be scaled from the z vector
+    graphdata->size = -1;
+    KapaPrepPlot (kapa, xVec->n, graphdata);
+    KapaPlotVector (kapa, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa, yVec->n, yVec->data.F32, "y");
+    KapaPlotVector (kapa, zVec->n, zScale->data.F32, "z");
+    psFree (zScale);
+    return true;
+}
+
+# else
+    # include "pmKapaPlots.h"
+
+    int pmKapaOpen (bool showWindow)
+{
+    return -1;
+}
+
+bool pmKapaClose ()
+{
+    return true;
+}
+
+bool pmKapaPlotVectorPair (psVector *xVec, psVector *yVec)
+{
+    return false;
+}
+
+bool pmKapaPlotVectorPair_AutoLimits_OpenGraph (int kapa, void *graphdata, psVector *xVec, psVector *yVec)
+{
+    return false;
+}
+
+bool pmKapaPlotVectorTriple_AutoLimits_OpenGraph (int kapa, void *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing)
+{
+    return false;
+}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/extras/pmKapaPlots.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/extras/pmKapaPlots.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/extras/pmKapaPlots.h	(revision 15358)
@@ -0,0 +1,35 @@
+/* @file  pmKapaPlots.h
+ * @brief functions to make plots with the external program 'kapa' 
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_KAPA_PLOTS_H
+#define PM_KAPA_PLOTS_H
+
+/// @addtogroup Extras Miscellaneous Funtions
+/// @{
+
+// move to psLib or psModules
+int pmKapaOpen (bool showWindow);
+bool pmKapaClose ();
+bool pmKapaPlotVectorPair (psVector *xVec, psVector *yVec);
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+
+// yes, this is an absurd name...
+bool pmKapaPlotVectorPair_AutoLimits_OpenGraph (int kapa, Graphdata *graphdata, psVector *xVec, psVector *yVec);
+bool pmKapaPlotVectorTriple_AutoLimits_OpenGraph (int kapa, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing);
+# else
+
+bool pmKapaPlotVectorPair_AutoLimits_OpenGraph (int kapa, void *graphdata, psVector *xVec, psVector *yVec);
+bool pmKapaPlotVectorTriple_AutoLimits_OpenGraph (int kapa, void *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing);
+# endif
+
+/// @}
+#endif // PM_KAPA_PLOTS_H
Index: /branches/eam_branch_20071023/psModules/src/extras/psIOBuffer.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/extras/psIOBuffer.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/extras/psIOBuffer.c	(revision 15358)
@@ -0,0 +1,113 @@
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <pslib.h>
+#include "psIOBuffer.h"
+
+static void psIOBufferFree (psIOBuffer *buffer)
+{
+    if (buffer == NULL)
+        return;
+
+    psFree (buffer->data);
+    return;
+}
+
+psIOBuffer *psIOBufferAlloc (int nBuffer)
+{
+
+    psIOBuffer *buffer = (psIOBuffer *)psAlloc(sizeof(psIOBuffer));
+    psMemSetDeallocator(buffer, (psFreeFunc) psIOBufferFree);
+
+    buffer->data = (char *) psAlloc (nBuffer + 1);
+
+    buffer->nAlloc = nBuffer + 1;
+    buffer->nReset = nBuffer;
+    buffer->nBlock = nBuffer / 2;
+    buffer->n = 0;
+    return (buffer);
+}
+
+bool psIOBufferFlush (psIOBuffer *buffer)
+{
+
+    if (buffer == NULL)
+        return false;
+    buffer->n = 0;
+    buffer->nAlloc = buffer->nReset;
+    buffer->data = psRealloc (buffer->data, buffer->nAlloc);
+    memset(buffer->data, '\0', buffer->nAlloc);
+
+    return true;
+}
+
+int psIOBufferRead (psIOBuffer *buffer, int fd)
+{
+
+    int Nread, Nfree;
+
+    if (fd == 0) {
+        /* pipe is closed */
+        return (0);
+    }
+
+    Nfree = buffer->nAlloc - buffer->n - 1;
+
+    // extend the data block if needed
+    if (Nfree < buffer->nBlock) {
+        buffer->nAlloc += 2*buffer->nBlock + 1;
+        buffer->data = psRealloc (buffer->data, buffer->nAlloc);
+        Nfree = buffer->nAlloc - buffer->n;
+        memset(buffer->data + buffer->n, '\0', Nfree);
+    }
+
+    // attempt to read from the fd into the buffer
+    Nread = read (fd, &buffer->data[buffer->n], buffer->nBlock);
+
+    if (Nread >= 0) {
+        buffer->n += Nread;
+        buffer->data[buffer->n] = 0;
+        return (Nread);
+    }
+
+    // check on exit status (try again if waiting for non-blocking fd)
+    if (Nread == -1) {
+        switch (errno) {
+        case EAGAIN:
+        case EIO:
+            /** no data available in pipe **/
+            return (-1);
+        default:
+            /** error reading from pipe **/
+            psError (PS_ERR_IO, true, "error on psIOBufferRead");
+            return (-2);
+        }
+    }
+    return (Nread);
+}
+
+/* read until buffer is empty (Nmax retries) */
+int psIOBufferReadEmpty (psIOBuffer *buffer, int Nmax, int fd)
+{
+
+    int i, status;
+
+    status = -1;
+    for (i = 0; (status != 0) && (i < Nmax); i++) {
+        status = psIOBufferRead (buffer, fd);
+        if (status == -2)
+            return false;
+        if (status == -1)
+            usleep (10000);
+        if (status > 0)
+            i = 0;
+    }
+    if (status == -1)
+        return false;
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/extras/psIOBuffer.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/extras/psIOBuffer.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/extras/psIOBuffer.h	(revision 15358)
@@ -0,0 +1,34 @@
+/* @file  IOBuffer.h
+ * @brief input/output character buffer
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:15 $
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PS_IO_BUFFER_H
+#define PS_IO_BUFFER_H
+
+/// @addtogroup Extras Miscellaneous Funtions
+/// @{
+
+typedef struct
+{
+    char *data;
+    int nAlloc;    // current size of allocated buffer
+    int nReset;    // size to set buffer after flush
+    int nBlock;    // number of bytes to try to read at a time
+    int n;    // current size of filled data
+}
+psIOBuffer;
+
+// psIOBuffer functions
+psIOBuffer *psIOBufferAlloc (int nBuffer);
+bool psIOBufferFlush (psIOBuffer *buffer);
+int psIOBufferRead (psIOBuffer *buffer, int fd);
+int psIOBufferReadEmpty (psIOBuffer *buffer, int maxRetries, int fd);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/extras/psPipe.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/extras/psPipe.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/extras/psPipe.c	(revision 15358)
@@ -0,0 +1,210 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <pslib.h>
+#include <psPipe.h>
+
+void closePipes (int *stdin_fd, int *stdout_fd, int *stderr_fd)
+{
+
+    if (stdin_fd[0]  != 0)
+        close (stdin_fd[0]);
+    if (stdin_fd[1]  != 0)
+        close (stdin_fd[0]);
+    if (stdout_fd[0] != 0)
+        close (stdout_fd[0]);
+    if (stdout_fd[1] != 0)
+        close (stdout_fd[1]);
+    if (stderr_fd[0] != 0)
+        close (stderr_fd[0]);
+    if (stderr_fd[1] != 0)
+        close (stderr_fd[1]);
+}
+
+static void psPipeFree (psPipe *pipe)
+{
+    return;
+}
+
+psPipe *psPipeAlloc ()
+{
+    psPipe *pipe = (psPipe *)psAlloc(sizeof(psPipe));
+    psMemSetDeallocator(pipe, (psFreeFunc) psPipeFree);
+
+    pipe->fd_stdin  = 0;
+    pipe->fd_stdout = 0;
+    pipe->fd_stderr = 0;
+    return (pipe);
+}
+
+psPipe *psPipeOpen (char *command)
+{
+
+    int stdin_fd[2], stdout_fd[2], stderr_fd[2], status;
+    pid_t pid;
+
+    memset(stdin_fd,  '\0', 2*sizeof(int));
+    memset(stdout_fd, '\0', 2*sizeof(int));
+    memset(stderr_fd, '\0', 2*sizeof(int));
+
+    if (pipe (stdin_fd)  < 0) {
+        psError (PS_ERR_UNKNOWN, true, "cannot create pipe file descriptor");
+        closePipes (stdin_fd, stdout_fd, stderr_fd);
+        return NULL;
+    }
+    if (pipe (stdout_fd) < 0) {
+        psError (PS_ERR_UNKNOWN, true, "cannot create pipe file descriptor");
+        closePipes (stdin_fd, stdout_fd, stderr_fd);
+        return NULL;
+    }
+    if (pipe (stderr_fd) < 0) {
+        psError (PS_ERR_UNKNOWN, true, "cannot create pipe file descriptor");
+        closePipes (stdin_fd, stdout_fd, stderr_fd);
+        return NULL;
+    }
+
+    psArray *cmd = psStringSplitArray (command, " ", false);
+    if (cmd->n <= 0) {
+        psError (PS_ERR_UNKNOWN, true, "empty command for pipe");
+        psFree (cmd);
+        closePipes (stdin_fd, stdout_fd, stderr_fd);
+        return NULL;
+    }
+
+    // create the command line array needed by execvp
+    char **argv = (char **)psAlloc((cmd->n+1)*sizeof(char *));
+    for (int i = 0; i < cmd->n; i++) {
+        argv[i] = cmd->data[i];
+    }
+    argv[cmd->n] = NULL;
+
+    pid = fork ();
+    if (!pid) { /* must be child process */
+        /* close the other ends of the pipes */
+        close (stdin_fd[1]);
+        close (stdout_fd[0]);
+        close (stderr_fd[0]);
+
+        /* tie our ends of the pipes to stdin, stdout, stderr */
+        dup2 (stdin_fd[0],  STDIN_FILENO);
+        dup2 (stdout_fd[1], STDOUT_FILENO);
+        dup2 (stderr_fd[1], STDERR_FILENO);
+
+        /* set all three unblocking */
+        setvbuf (stdin,  (char *) NULL, _IONBF, BUFSIZ);
+        setvbuf (stdout, (char *) NULL, _IONBF, BUFSIZ);
+        setvbuf (stderr, (char *) NULL, _IONBF, BUFSIZ);
+
+        status = execvp (argv[0], argv);
+
+        // this statement exits the child, not the parent, process
+        exit (1);
+    }
+    psFree (cmd);
+    psFree (argv);
+
+    if (pid == -1) {
+        psError (PS_ERR_UNKNOWN, true, "unable to create child process");
+        closePipes (stdin_fd, stdout_fd, stderr_fd);
+        return NULL;
+    }
+
+    /* close the other ends of the pipes */
+    close (stdin_fd[0]);
+    stdin_fd[0]  = 0;
+    close (stdout_fd[1]);
+    stdout_fd[1] = 0;
+    close (stderr_fd[1]);
+    stderr_fd[1] = 0;
+
+    /* make the pipes non-blocking */
+    fcntl (stdin_fd[1],  F_SETFL, O_NONBLOCK);
+    fcntl (stdout_fd[0], F_SETFL, O_NONBLOCK);
+    fcntl (stderr_fd[0], F_SETFL, O_NONBLOCK);
+
+    psPipe *pipe = psPipeAlloc();
+
+    pipe->pid    = pid;
+    pipe->fd_stdin  = stdin_fd[1];
+    pipe->fd_stdout = stdout_fd[0];
+    pipe->fd_stderr = stderr_fd[0];
+
+    return (pipe);
+}
+
+// this function returns the exit status of the called function
+// or a value > 255 on an error
+int psPipeClose (psPipe *pipe)
+{
+    int close_status;
+    int exit_status;
+    int wait_status;
+    int result;
+
+    PS_ASSERT_PTR_NON_NULL(pipe, false);
+
+    close_status = true;
+    if (close (pipe->fd_stdin) != 0) {
+        psError(PS_ERR_IO, true, "error closing the pipe stdin (pid %d, error %s)\n", pipe->pid, strerror(errno));
+        close_status = false;
+    }
+    if (close (pipe->fd_stdout) != 0) {
+        psError(PS_ERR_IO, true, "error closing the pipe stdout (pid %d, error %s)\n", pipe->pid, strerror(errno));
+        close_status = false;
+    }
+    if (close (pipe->fd_stderr) != 0) {
+        psError(PS_ERR_IO, true, "error closing the pipe sterr (pid %d, error %s)\n", pipe->pid, strerror(errno));
+        close_status = false;
+    }
+
+    // we expect the child process to have exited.
+    // wait for the exit condition, but no longer than 100ms
+    for (int i = 0; i < 10; i++) {
+        result = waitpid (pipe->pid, &wait_status, WNOHANG);
+        switch (result) {
+        case -1:   // error on waitpid
+            switch (errno) {
+            case ECHILD:
+                psError(PS_ERR_IO, true, "unknown PID, not a child process: %d\n", pipe->pid);
+                return 0x100;
+            default:
+                psAbort("unexpected response to waitpid: %d\n", result);
+            }
+            break;
+
+        case 0:   // child not yet exited
+            usleep (10000);
+            continue;
+
+        default:
+            if (result != pipe->pid) {
+                psAbort("waitpid error: mis-matched PID (%d vs %d).  programming error\n", result, pipe->pid);
+            }
+            if (WIFEXITED(wait_status)) {
+                exit_status = WEXITSTATUS(wait_status);
+                if (close_status) {
+                    return exit_status;
+                } else {
+                    return (0x100);
+                }
+            }
+            if (WIFSIGNALED(wait_status)) {
+                psError(PS_ERR_IO, true, "job %d exited on signal %d\n", pipe->pid, WTERMSIG(wait_status));
+                return (0x100 + WTERMSIG(wait_status));
+            }
+            if (WIFSTOPPED(wait_status)) {
+                psAbort("waitpid returns 'stopped' programming error\n");
+            }
+        }
+    }
+    psError(PS_ERR_IO, true, "child process pid %d did not exit\n", pipe->pid);
+    return 0x100;
+}
Index: /branches/eam_branch_20071023/psModules/src/extras/psPipe.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/extras/psPipe.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/extras/psPipe.h	(revision 15358)
@@ -0,0 +1,33 @@
+/* @file  psPipe.h
+ * @brief 3-stream pipe 
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:15 $
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PS_PIPE_H
+#define PS_PIPE_H
+
+/// @addtogroup Extras Miscellaneous Funtions
+/// @{
+
+// move these to pslib??
+typedef struct
+{
+    int pid;
+    int fd_stdin;
+    int fd_stdout;
+    int fd_stderr;
+}
+psPipe;
+
+// psPipe functions
+psPipe *psPipeAlloc ();
+psPipe *psPipeOpen (char *command);
+int     psPipeClose (psPipe *pipe);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/extras/psVectorBracket.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/extras/psVectorBracket.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/extras/psVectorBracket.c	(revision 15358)
@@ -0,0 +1,137 @@
+/** @file  psVectorBracket.c
+ *
+ *  Vector Bracketing tools
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-12-10 18:27:26 $
+ *
+ *  Copyright 2006 Institute for Astronomy, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+#include "psVectorBracket.h"
+
+// return the last entry below or first entry above key value
+int psVectorBracket(const psVector *index, psF32 key, bool above)
+{
+
+    int N;
+    int Nlo = 0;
+    int Nhi = index->n;
+
+    if (above) {
+        while (Nhi - Nlo > 10) {
+            N = 0.5*(Nlo + Nhi);
+            if (index->data.F32[N] > key) {
+                Nhi = N;
+            } else {
+                Nlo = N - 1;
+            }
+        }
+        // at this point, index[Nhi] > key >= index[Nlo]
+        N = Nlo;
+        while ((index->data.F32[N] <= key) && (N < Nhi)) {
+            N++;
+        }
+        return (N);
+    }
+    while (Nhi - Nlo > 10) {
+        N = 0.5*(Nlo + Nhi);
+        if (index->data.F32[N] < key) {
+            Nlo = N;
+        } else {
+            Nhi = N + 1;
+        }
+    }
+    // at this point, index[Nhi] >= key > index[Nlo]
+    N = Nhi;
+    while ((index->data.F32[N] >= key) && (N > Nlo)) {
+        N--;
+    }
+    return (N);
+}
+
+// return the last entry below or first entry above key value (reverse sorted input)
+int psVectorBracketDescend(const psVector *index, psF32 key, bool above)
+{
+
+    int N;
+    int Nhi = 0;
+    int Nlo = index->n;
+
+    if (above) {
+        while (Nlo - Nhi > 10) {
+            N = 0.5*(Nhi + Nlo);
+            if (index->data.F32[N] < key) {
+                Nlo = N;
+            } else {
+                Nhi = N - 1;
+            }
+        }
+        // at this point, index[Nhi] >= key > index[Nlo]
+        N = Nhi;
+        while ((index->data.F32[N] >= key) && (N < Nlo)) {
+            N++;
+        }
+        return (N);
+    }
+    while (Nlo - Nhi > 10) {
+        N = 0.5*(Nhi + Nlo);
+        if (index->data.F32[N] > key) {
+            Nhi = N;
+        } else {
+            Nlo = N + 1;
+        }
+    }
+    // at this point, index[Nhi] > key >= index[Nlo]
+    N = Nlo;
+    while ((index->data.F32[N] <= key) && (N > Nhi)) {
+        N--;
+    }
+    return (N);
+}
+
+// search for the bins bounding key in index, interpolate the corresponding values
+psF32 psVectorInterpolate(const psVector *index, const psVector *value, psF32 key)
+{
+
+    int n0 = 0;
+    int n1 = 0;
+
+    // extrapolate at ends
+    if (key < index->data.F32[0]) {
+        n0 = 0;
+        n1 = 1;
+    }
+
+    // extrapolate at ends
+    if (key > index->data.F32[index->n-1]) {
+        n0 = index->n-2;
+        n1 = index->n-1;
+    }
+
+    if (n1 == 0) {
+        n0 = psVectorBracket (index, key, FALSE);
+        n1 = n0 + 1;
+    }
+
+    if (n0 == index->n-1) {
+        n1 = n0;
+        n0 = n1 - 1;
+    }
+
+    float dy = value->data.F32[n1] - value->data.F32[n0];
+    float dx = index->data.F32[n1] - index->data.F32[n0];
+    float dX = key - index->data.F32[n0];
+    float dY = dX * (dy/dx);
+    float result = value->data.F32[n0] + dY;
+    return result;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/extras/psVectorBracket.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/extras/psVectorBracket.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/extras/psVectorBracket.h	(revision 15358)
@@ -0,0 +1,22 @@
+/* @file  psVectorBracket.h
+ * @brief vector bracket functions
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:15 $
+ * Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+ */
+
+# ifndef PS_VECTOR_BRACKET_H
+# define PS_VECTOR_BRACKET_H
+
+/// @addtogroup Extras Miscellaneous Funtions
+/// @{
+
+int psVectorBracket(const psVector *index, psF32 key, bool above);
+int psVectorBracketDescend(const psVector *index, psF32 key, bool above);
+psF32 psVectorInterpolate(const psVector *index, const psVector *value, psF32 key);
+
+/// @}
+# endif /* PS_VECTOR_BRACKET_H */
Index: /branches/eam_branch_20071023/psModules/src/imcombine/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/.cvsignore	(revision 15358)
@@ -0,0 +1,6 @@
+.deps
+.libs
+Makefile
+Makefile.in
+*.la
+*.lo
Index: /branches/eam_branch_20071023/psModules/src/imcombine/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/Makefile.am	(revision 15358)
@@ -0,0 +1,27 @@
+noinst_LTLIBRARIES = libpsmodulesimcombine.la
+
+libpsmodulesimcombine_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS)
+libpsmodulesimcombine_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+
+libpsmodulesimcombine_la_SOURCES = \
+	pmReadoutCombine.c	\
+	pmStack.c		\
+	pmStackReject.c		\
+	pmSubtraction.c		\
+	pmSubtractionKernels.c	\
+	pmSubtractionStamps.c	\
+	pmSubtractionMatch.c	\
+	pmSubtractionParams.c
+
+pkginclude_HEADERS = \
+	pmImageCombine.h \
+	pmReadoutCombine.h	\
+	pmStack.h		\
+	pmStackReject.h		\
+	pmSubtraction.h		\
+	pmSubtractionKernels.h	\
+	pmSubtractionStamps.h	\
+	pmSubtractionMatch.h	\
+	pmSubtractionParams.h
+
+CLEANFILES = *~
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmImageCombine.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmImageCombine.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmImageCombine.c	(revision 15358)
@@ -0,0 +1,683 @@
+/** @file  pmImageCombine.c
+ *
+ *  This file will perform image combination of several images of the
+ *  same field, produce a list of questionable pixels, then tag some
+ *  of those pixels as cosmic rays.
+ *
+ *  @author Paul Price, IfA (original prototype)
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-04-04 22:42:48 $
+ *
+ *  XXX: pmRejectPixels() has a known bug with the pmImageTransform() call.
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <config.h>
+#include <stdio.h>
+#include <math.h>
+#include "pslib.h"
+
+#define PIXEL_LIST_BUFFER 100           // Size of the pixel list buffer
+
+// Data structure for use as a buffer in combining pixels
+typedef struct
+{
+    psVector *pixels;                   // Pixel values
+    psVector *masks;                    // Pixel masks
+    psVector *errors;                   // Pixel errors
+    psStats *stats;                     // Statistics to use with combination
+}
+combineBuffer;
+
+void combineBufferFree(combineBuffer *buffer)
+{
+    psFree(buffer->pixels);
+    psFree(buffer->masks);
+    psFree(buffer->errors);
+    psFree(buffer->stats);
+}
+
+combineBuffer *combineBufferAlloc(long numImages // Number of images that will be combined
+                                 )
+{
+    combineBuffer *buffer = psAlloc(sizeof(combineBuffer));
+    psMemSetDeallocator(buffer, (psFreeFunc)combineBufferFree);
+
+    buffer->pixels = psVectorAlloc(numImages, PS_TYPE_F32);
+    buffer->masks = psVectorAlloc(numImages, PS_TYPE_MASK);
+    buffer->errors = psVectorAlloc(numImages, PS_TYPE_F32);
+    buffer->stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+
+    return buffer;
+}
+
+
+static bool combinePixels(psImage *combine, // Combined image, for output
+                          psArray *questionablePixels, // Array of rejection masks
+                          int x, int y, // Position in the images
+                          const psArray *images, // Array of input images
+                          const psArray *errors, // Array of input error images
+                          const psArray *masks, // Array of input masks
+                          psU32 maskVal, // Mask value
+                          psS32 numIter, // Number of rejection iterations
+                          psF32 sigmaClip, // Number of standard deviations at which to reject
+                          combineBuffer *buffer // Buffer for combination; to avoid multiple allocations
+                         )
+{
+    assert(combine);
+    assert(x >= 0 && x < combine->numCols);
+    assert(y >= 0 && y < combine->numRows);
+    assert(images);
+    int numImages = images->n;          // Number of images to combine
+    if (masks) {
+        assert(masks->n == numImages);
+    }
+    if (errors) {
+        assert(errors->n == numImages);
+    }
+    assert(numIter >= 0);
+    assert(sigmaClip > 0);
+    assert(!questionablePixels || (questionablePixels && questionablePixels->n == numImages));
+
+    if (buffer) {
+        psMemIncrRefCounter(buffer);
+    } else {
+        buffer = combineBufferAlloc(numImages);
+    }
+
+    psVector *pixelData = buffer->pixels; // Values for the pixel of interest
+    psVector *pixelErrors = buffer->errors; // Errors for the pixel of interest
+    psVector *pixelMasks = buffer->masks; // Masks for the pixel of interest
+    psStats *stats = buffer->stats;     // Statistics for combination
+
+    //
+    // Loop through each image, extract the pixel/mask/error data into psVectors.
+    //
+    if (!masks) {
+        psVectorInit(pixelMasks, 0);
+    }
+    if (!errors) {
+        pixelErrors = NULL;
+    }
+    for (int i = 0; i < numImages; i++) {
+        // Set the pixel data
+        psImage *image = images->data[i]; // Image of interest
+        pixelData->data.F32[i] = image->data.F32[y][x];
+        // Set the pixel mask data, if necessary
+        if (masks) {
+            psImage *mask = masks->data[i]; // Mask of interest
+            pixelMasks->data.U8[i] = mask->data.U8[y][x];
+        }        // Set the pixel error data, if necessary
+        if (errors) {
+            psImage *error = errors->data[i]; // Error image of interest
+            pixelErrors->data.F32[i] = error->data.F32[y][x];
+        }
+    }
+
+    //
+    // Iterate on the pixels, rejecting outliers
+    //
+    for (int iter = 0; iter < numIter; iter++) {
+        // Combine all the pixels, using the specified stat.
+        if (!psVectorStats(stats, pixelData, pixelErrors, pixelMasks, maskVal)) {
+            combine->data.F32[y][x] = NAN;
+            psFree(buffer);
+            return false;
+        }
+        float combinedPixel = stats->sampleMean; // Value of the combination
+
+        if (iter == 0) {
+            // Save the value produced with no rejection, since it may be useful later
+            // (if the rejection turns out to be unnecessary)
+            combine->data.F32[y][x] = combinedPixel;
+        }
+
+        //
+        // Reject all pixels that lie more that sigmaClip standard deviations from
+        // the combined pixel value.
+        //
+        int numRejects = 0;     // Number of rejections
+        float stdev = stats->sampleStdev;
+        for (int i = 0; i < numImages; i++) {
+            if (!(pixelMasks->data.U8[i] & maskVal) &&
+                    fabs(pixelData->data.F32[i] - combinedPixel) > sigmaClip * stdev) {
+                // Reject pixel as questionable
+                numRejects++;
+                pixelMasks->data.U8[i] = maskVal;
+                if (questionablePixels) {
+                    // Mark the pixel as questionable
+                    psPixels *qp = questionablePixels->data[i]; // Questionable pixels for this image
+                    int qpNum = qp->n; // Number of QPs in the image of interest
+                    if (qpNum >= qp->nalloc) {
+                        // Grow dynamically, if required
+                        qp = psPixelsRealloc(qp, qp->nalloc + PIXEL_LIST_BUFFER);
+                        questionablePixels->data[i] = qp;
+                    }
+                    qp->data[qpNum].x = x;
+                    qp->data[qpNum].y = y;
+                    qp->n++;
+                }
+            }
+        }
+
+        //
+        // If the number of rejected pixels is zero, then there's no point continuing the loop.
+        //
+        if (numRejects == 0) {
+            break;
+        }
+
+        //
+        // XXX: Is it possible to have all pixels rejected?  If so, we should exit the loop.
+        //
+    }
+
+    psFree(buffer);
+    return true;
+}
+
+psImage *pmCombineImages(
+    psImage *combine,                   ///< Combined image (output)
+    psArray **questionablePixels,       ///< Array of rejection masks
+    const psArray *images,              ///< Array of input images
+    const psArray *errors,              ///< Array of input error images
+    const psArray *masks,               ///< Array of input masks
+    psU32 maskVal,                      ///< Mask value
+    const psPixels *pixels,             ///< Pixels to combine
+    psS32 numIter,                      ///< Number of rejection iterations
+    psF32 sigmaClip                     ///< Number of standard deviations at which to reject
+)
+{
+    psTrace("psModules.imcombine", 3, "Calling pmCombineImages(%ld)\n", images->n);
+
+    PS_ASSERT_ARRAY_NON_NULL(images, NULL);
+    PS_ASSERT_INT_POSITIVE(images->n, NULL);
+    long numImages = images->n;          // Number of images
+    int numCols = ((psImage*)images->data[0])->numCols; // Size in x
+    int numRows = ((psImage*)images->data[0])->numRows; // Size in y
+
+    if (combine) {
+        PS_ASSERT_IMAGE_NON_NULL(combine, NULL);
+        PS_ASSERT_IMAGE_SIZE(combine, numCols, numRows, NULL);
+    }
+    if (questionablePixels && !*questionablePixels) {
+        PS_ASSERT_ARRAY_NON_NULL(*questionablePixels, NULL);
+        PS_ASSERT_ARRAY_SIZE(*questionablePixels, numImages, 0);
+    }
+    for (int i = 1; i < images->n; i++) {
+        psImage *image = images->data[i]; // Image of interest
+        PS_ASSERT_IMAGE_TYPE(image, PS_TYPE_F32, NULL);
+        PS_ASSERT_IMAGE_SIZE(image, numCols, numRows, NULL);
+    }
+    if (errors) {
+        PS_ASSERT_ARRAYS_SIZE_EQUAL(images, errors, NULL);
+        for (int i = 0; i < images->n; i++) {
+            psImage *error = errors->data[i];
+            PS_ASSERT_IMAGE_SIZE(error, numCols, numRows, NULL);
+            PS_ASSERT_IMAGE_TYPE(error, PS_TYPE_F32, NULL);
+        }
+    }
+    if (masks) {
+        PS_ASSERT_ARRAYS_SIZE_EQUAL(images, masks, NULL);
+        for (int i = 0; i < images->n; i++) {
+            psImage *mask  = masks->data[i];
+            PS_ASSERT_IMAGE_SIZE(mask, numCols, numRows, NULL);
+            PS_ASSERT_IMAGE_TYPE(mask, PS_TYPE_MASK, NULL);
+        }
+    }
+    PS_ASSERT_INT_POSITIVE(numIter, NULL);
+    PS_ASSERT_FLOAT_LARGER_THAN(sigmaClip, 0.0, NULL);
+
+    // Allocate and initialize the combined image, if necessary.
+    if (!combine) {
+        combine = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        if (pixels) {
+            // Set everything we're not working on to NAN
+            psImageInit(combine, NAN);
+        }
+    }
+
+    //
+    // Allocate the questionablePixels psArray, if necesssary, then create a psPixels
+    // struct for each image.
+    //
+    if (questionablePixels) {
+        if (*questionablePixels == NULL) {
+            *questionablePixels = psArrayAlloc(numImages);
+        } else if ((*questionablePixels)->n != numImages) {
+            *questionablePixels = psArrayRealloc(*questionablePixels, numImages);
+        }
+        for (int i = 0; i < numImages; i++) {
+            psFree((*questionablePixels)->data[i]);
+            (*questionablePixels)->data[i] = psPixelsAlloc(PIXEL_LIST_BUFFER);
+        }
+    }
+
+    combineBuffer *buffer = combineBufferAlloc(numImages); // Buffer for combination
+
+    if (pixels) {
+        // Only those specified pixels should be combined.
+
+        for (int p = 0; p < pixels->n; p++) {
+            int x = pixels->data[p].x; // Column of interest
+            int y = pixels->data[p].y; // Row of interest
+
+            if (!combinePixels(combine, questionablePixels ? *questionablePixels : NULL, x, y,
+                               images, errors, masks, maskVal, numIter, sigmaClip, buffer)) {
+                // Bad pixel --- no big deal
+                psErrorClear();
+            }
+        }
+    } else {
+        //
+        // We get here if there is a NULL list of pixels to combine.
+        // Therefore, we combine all pixels in all images.
+        //
+
+        //
+        // Loop over all pixels in all images, set the appropriate data, mask,
+        // error vectors, call psVectorStats(), and set the result in the
+        // combine image.
+        //
+        for (int y = 0; y < numRows; y++) {
+            for (int x = 0; x < numCols; x++) {
+                if (!combinePixels(combine, questionablePixels ? *questionablePixels : NULL, x, y,
+                                   images, errors, masks, maskVal, numIter, sigmaClip, buffer)) {
+                    // Bad pixel --- no big deal
+                    psErrorClear();
+                }
+            }
+        }
+    }
+
+    psFree(buffer);
+
+    psTrace("psModules.imcombine", 3, "Exiting pmCombineImages(%ld)\n", images->n);
+    return combine;
+}
+
+
+/******************************************************************************
+XXX: Directly from Paul Price
+ *****************************************************************************/
+static psF32 CalcGradient(
+    psImage *image,
+    psImage *imageMask,
+    psS32 x,
+    psS32 y
+)
+{
+    psTrace("psModules.imcombine", 4, "Calling CalcGradient(%d, %d)\n", x, y);
+    int num = 0;
+    psVector *pixels = psVectorAlloc(8, PS_TYPE_F32); // Array of pixels
+    psVector *mask = psVectorAlloc(8, PS_TYPE_U8); // Corresponding mask
+
+    // Get limits
+    int xMin = PS_MAX(x - 1, 0);
+    int xMax = PS_MIN(x + 1, image->numCols - 1);
+    int yMin = PS_MAX(y - 1, 0);
+    int yMax = PS_MIN(y + 1, image->numRows - 1);
+    if (imageMask != NULL) {
+        for (int j = yMin; j <= yMax; j++) {
+            for (int i = xMin; i <= xMax; i++) {
+                if ((i != x) && (j != y) && (0 == imageMask->data.U8[j][i])) {
+                    pixels->data.F32[num] = image->data.F32[j][i];
+                    mask->data.U8[num] = 0;
+                    num++;
+                } else {
+                    mask->data.U8[num] = 1;
+                }
+            }
+        }
+    } else {
+        //
+        // This code is simply the previous loop without the imageMask.
+        // XXX: Consider restructuring this.
+        //
+        for (int j = yMin; j <= yMax; j++) {
+            for (int i = xMin; i <= xMax; i++) {
+                if ((i != x) && (j != y)) {
+                    pixels->data.F32[num] = image->data.F32[j][i];
+                    mask->data.U8[num] = 0;
+                    num++;
+                } else {
+                    mask->data.U8[num] = 1;
+                }
+            }
+        }
+    }
+
+    pixels->n = num;
+    mask->n = num;
+
+    // Get the median
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+    psVectorStats(stats, pixels, NULL, mask, 1);
+    float median = stats->sampleMedian;
+    psFree(stats);
+    psFree(pixels);
+    psFree(mask);
+
+    psTrace("psModules.imcombine", 4, "Exiting CalcGradient(%d, %d)\n", x, y);
+    return(median / image->data.F32[y][x]);
+}
+
+/******************************************************************************
+DetermineRegion(image, myOutToIn): for a psImage and a psPlaneTransform to that
+image, this routine determines the size of the input image which maps to that
+image, and returns the result in a psRegion struct.
+
+XXX: Basically, this routine is only guaranteed to work if the transform is
+linear.
+
+XXX: Shouldn't this functionality be part of psImageTransform()?
+ *****************************************************************************/
+static psRegion DetermineRegion(psImage *image,
+                                psPlaneTransform *myOutToIn)
+{
+    psTrace("psModules.imcombine", 4, "Calling DetermineRegion()\n");
+    psRegion myRegion;
+    myRegion.x0 = PS_MAX_F32;
+    myRegion.x1 = PS_MIN_F32;
+    myRegion.y0 = PS_MAX_F32;
+    myRegion.y1 = PS_MIN_F32;
+    psPlane in;
+    psPlane out;
+
+    in.x = 0.0;
+    in.y = 0.0;
+
+    psPlaneTransformApply(&out, myOutToIn, &in);
+    if (out.x < myRegion.x0) {
+        myRegion.x0 = out.x;
+    }
+    if (out.x > myRegion.x1) {
+        myRegion.x1 = out.x;
+    }
+    if (out.y < myRegion.y0) {
+        myRegion.y0 = out.y;
+    }
+    if (out.y > myRegion.y1) {
+        myRegion.y1 = out.y;
+    }
+
+    in.x = (psF32) (image->numCols);
+    in.y = 0.0;
+    psPlaneTransformApply(&out, myOutToIn, &in);
+    if (out.x < myRegion.x0) {
+        myRegion.x0 = out.x;
+    }
+    if (out.x > myRegion.x1) {
+        myRegion.x1 = out.x;
+    }
+    if (out.y < myRegion.y0) {
+        myRegion.y0 = out.y;
+    }
+    if (out.y > myRegion.y1) {
+        myRegion.y1 = out.y;
+    }
+
+    in.x = (psF32) (image->numCols);
+    ;
+    in.y = 0.0;
+    psPlaneTransformApply(&out, myOutToIn, &in);
+    if (out.x < myRegion.x0) {
+        myRegion.x0 = out.x;
+    }
+    if (out.x > myRegion.x1) {
+        myRegion.x1 = out.x;
+    }
+    if (out.y < myRegion.y0) {
+        myRegion.y0 = out.y;
+    }
+    if (out.y > myRegion.y1) {
+        myRegion.y1 = out.y;
+    }
+
+    in.x = (psF32) (image->numCols);
+    in.y = (psF32) (image->numRows);
+    psPlaneTransformApply(&out, myOutToIn, &in);
+    if (out.x < myRegion.x0) {
+        myRegion.x0 = out.x;
+    }
+    if (out.x > myRegion.x1) {
+        myRegion.x1 = out.x;
+    }
+    if (out.y < myRegion.y0) {
+        myRegion.y0 = out.y;
+    }
+    if (out.y > myRegion.y1) {
+        myRegion.y1 = out.y;
+    }
+
+    psTrace("psModules.imcombine", 4, "Exiting DetermineRegion()\n");
+    return(myRegion);
+}
+
+/******************************************************************************
+XXX: Don't we have a psLib function for this?
+ *****************************************************************************/
+static psImage *ImageConvertF32(psImage *image)
+{
+    psTrace("psModules.imcombine", 4, "Calling ImageConvertF32()\n");
+    psImage *imgF32 = psImageAlloc(image->numCols, image->numRows, PS_TYPE_F32);
+
+    for (psS32 i = 0 ; i < image->numRows ; i++) {
+        for (psS32 j = 0 ; j < image->numCols ; j++) {
+            imgF32->data.F32[i][j] = (psF32) image->data.U8[i][j];
+        }
+    }
+
+    psTrace("psModules.imcombine", 4, "Exiting ImageConvertF32()\n");
+    return(imgF32);
+}
+
+
+//
+// The following macros define how big the initial pixel list will be, and
+// how much it should be incremented when realloc'ed.
+//
+#define PS_REJECT_PIXEL_INITIAL_PIXEL_LIST_LENGTH 100
+#define PS_REJECT_PIXEL_INITIAL_PIXEL_LIST_LENGTH_INC 100
+/******************************************************************************
+pmRejectPixels(images, errors, inToOut, outToIn, rejThreshold,
+gradLimit)
+
+XXX: Optimization: we don't need to transform the entire mask image.
+XXX: The inToOut and outToIn transforms are confusing.  Verify that what
+     I think they mean syncs with PWP.
+ *****************************************************************************/
+psArray *pmRejectPixels(
+    const psArray *images,              ///< Array of input images
+    const psArray *masks,               ///< Array of input image masks
+    const psArray *errors,              ///< The pixels which were rejected in the combination
+    const psArray *inToOut,             ///< Transformation from input to output system
+    const psArray *outToIn,             ///< Transformation from output to input system
+    psF32 rejThreshold,                 ///< Rejection threshold
+    psF32 gradLimit                     ///< Gradient limit
+)
+{
+    psTrace("psModules.imcombine", 3, "Calling pmRejectPixels()\n");
+    PS_ASSERT_PTR_NON_NULL(images, NULL);
+    for (psS32 im = 0 ; im < images->n ; im++) {
+        psImage *tmpImage = (psImage *) images->data[im];
+        PS_ASSERT_IMAGE_NON_NULL(tmpImage, NULL);
+        PS_ASSERT_IMAGE_NON_EMPTY(tmpImage, NULL);
+        PS_ASSERT_IMAGE_TYPE(tmpImage, PS_TYPE_F32, NULL);
+        if (masks != NULL) {
+            PS_ASSERT_INT_EQUAL(images->n, masks->n, NULL);
+            psImage *tmpMask = (psImage *) masks->data[im];
+            PS_ASSERT_IMAGE_NON_NULL(tmpMask, NULL);
+            PS_ASSERT_IMAGE_NON_EMPTY(tmpMask, NULL);
+            PS_ASSERT_IMAGE_TYPE(tmpMask, PS_TYPE_F32, NULL);
+            PS_ASSERT_IMAGES_SIZE_EQUAL(tmpImage, tmpMask, NULL);
+        }
+        PS_ASSERT_IMAGES_SIZE_EQUAL(((psImage *) images->data[0]), tmpImage, NULL);
+    }
+    PS_ASSERT_PTR_NON_NULL(errors, NULL);
+    PS_ASSERT_PTR_NON_NULL(inToOut, NULL);
+    PS_ASSERT_PTR_NON_NULL(outToIn, NULL);
+    // Ensure that the psArray parameters have an element for each image.
+    psS32 numImages = images->n;
+    PS_ASSERT_INT_EQUAL(numImages, errors->n, NULL);
+    PS_ASSERT_INT_EQUAL(numImages, inToOut->n, NULL);
+    PS_ASSERT_INT_EQUAL(numImages, outToIn->n, NULL);
+
+    //
+    // Create the psArray of psPixelLists, one for each image, for rejected pixels.
+    //
+    psArray *rejects = psArrayAlloc(numImages);
+    for (psS32 im = 0 ; im < numImages ; im++) {
+        rejects->data[im] = (psPtr *) psPixelsAlloc(PS_REJECT_PIXEL_INITIAL_PIXEL_LIST_LENGTH);
+        ((psPixels *)(rejects->data[im]))->n = ((psPixels *)(rejects->data[im]))->nalloc;
+        psPixels *pixels = (psPixels *) rejects->data[im];
+        pixels->n = 0;
+    }
+    //
+    // rPtr is used to maintain a count of the questionable pixels for each image.
+    //
+    psVector *rPtr = psVectorAlloc(numImages, PS_TYPE_S32);
+    psVectorInit(rPtr, 0);
+
+    psS32 numCols = ((psImage *) images->data[0])->numCols;
+    psS32 numRows = ((psImage *) images->data[0])->numRows;
+    psRegion myRegion = psRegionSet(0, numCols-1, 0, numRows-1);
+    psU32 maskVal = 1;  // XXX: Is this appropriate?
+
+    psPlane *inCoords = psAlloc(sizeof(psPlane));
+    psPlane *outCoords = psAlloc(sizeof(psPlane));
+
+    for (psS32 im = 0 ; im < numImages ; im++) {
+        //
+        // Extract data from psArrays.
+        //
+        psPixels *pixelList = (psPixels *) errors->data[im];
+
+        psImage *currImage = (psImage *) images->data[im];
+        myRegion.x0 = 0;
+        myRegion.x1 = currImage->numCols;
+        myRegion.y0 = 0;
+        myRegion.y1 = currImage->numRows;
+        psPlaneTransform *myInToOut = (psPlaneTransform *) inToOut->data[im];
+        psPlaneTransform *myOutToIn = (psPlaneTransform *) outToIn->data[im];
+
+        //
+        // Create a psU8 mask image from the list of cosmic pixels.
+        //
+        psImage *maskImage = NULL;
+        maskImage = psPixelsToMask(maskImage, pixelList, myRegion, maskVal);
+        psImage *maskImageF32 = ImageConvertF32(maskImage);
+
+        //
+        // Transform that mask image into detector coordinate space
+        //
+        psRegion myRegionXForm = DetermineRegion(maskImageF32, myOutToIn);
+        psImage *transformedImage = psImageTransform(NULL, NULL, maskImageF32, NULL,
+                                    0, myOutToIn, myRegionXForm, NULL,
+                                    PS_INTERPOLATE_BILINEAR, 0);
+
+        //
+        // Loop over all cosmic pixels.  Transform their coords to detector space.
+        // If the value of the transformed mask is larger than rejThreshold, then
+        // this might be a cosmic ray pixel.  We then calculate the mean gradient
+        // in other images.
+        //
+
+        psImageInterpolateOptions *interp = psImageInterpolateOptionsAlloc(PS_INTERPOLATE_BILINEAR,
+                                                                           transformedImage, NULL, NULL,
+                                                                           0, 0.0, 0.0, 0, 0, 0.0);
+
+        for (psS32 p = 0 ; p < pixelList->n ; p++) {
+            inCoords->x = 0.5 + (psF32) (pixelList->data[p]).x;
+            inCoords->y = 0.5 + (psF32) (pixelList->data[p]).y;
+            psPlaneTransformApply(outCoords, myInToOut, inCoords);
+            double maskVal;
+            if (!psImageInterpolate(&maskVal, NULL, NULL, outCoords->x, outCoords->y, interp)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to interpolate image.");
+                psFree(interp);
+                psFree(maskImage);
+                psFree(maskImageF32);
+                psFree(transformedImage);
+                psFree(inCoords);
+                psFree(outCoords);
+                psFree(rejects);
+                return NULL;
+            }
+            if (maskVal > rejThreshold) {
+
+                // This is a possible cosmic array pixel.  We must calculate the gradient
+                // at this location in all input images.
+                psF32 meanGrads = 0.0;
+                psS32 numGrads = 0;
+                //
+                // Loop through all other images, calculate their mean gradient.
+                //
+                for (psS32 otherImg = 0 ; otherImg < numImages ; otherImg++) {
+                    if (im != otherImg) {
+                        // Map the outCoords to inCoords that for otherImg space.
+                        psImage *tmpMask = NULL;
+                        if (masks != NULL) {
+                            tmpMask = masks->data[otherImg];
+                        }
+                        psPlaneTransformApply(inCoords,
+                                              (psPlaneTransform * )outToIn->data[otherImg],
+                                              outCoords);
+                        psS32 xPix = (int)(inCoords->x + 0.5);
+                        psS32 yPix = (int)(inCoords->y + 0.5);
+                        if ((xPix >= 0) && (xPix <= ((psImage*)(images->data[otherImg]))->numCols - 1) &&
+                                (yPix >= 0) && (yPix <= ((psImage*)(images->data[otherImg]))->numRows - 1)) {
+                            meanGrads += CalcGradient(images->data[otherImg], tmpMask, xPix, yPix);
+                            numGrads++;
+                        }
+                    }
+                }
+                if (numGrads > 0) {
+                    meanGrads /= (psF32) numGrads;
+                } else {
+                    // XXX: my idea.  Verify with PWP:
+                    meanGrads = 1.0 + gradLimit;
+                }
+
+                // XXX: The SDRS and the prototype code differ significantly here:
+                // if (CalcGradient(inputs->data[i], pixelList->data.x, pixelList->data.y) < (gradLimit * meanGrads)) {
+                if (meanGrads < gradLimit) {
+                    //
+                    // Add this to the list of questionable pixels.  We must ensure that the
+                    // pixelList is large enough; if not, we realloc()
+                    //
+                    psS32 ptr = rPtr->data.S32[im];
+                    psPixels *pixelListPtr = (psPixels *) rejects->data[im];
+                    if (ptr >= pixelListPtr->nalloc) {
+                        rejects->data[im] = (psPtr *) psPixelsRealloc(((psPixels *) rejects->data[im]),
+                                            ((((psPixels *) rejects->data[im])->nalloc) + PS_REJECT_PIXEL_INITIAL_PIXEL_LIST_LENGTH_INC));
+                        // XXX: Can the realloc() fail?  Must we check for NULL?
+                    }
+
+                    ((psPixels *) rejects->data[im])->data[ptr].x = (pixelList->data[p]).x;
+                    ((psPixels *) rejects->data[im])->data[ptr].y = (pixelList->data[p]).y;
+                    (rPtr->data.S32[im])++;
+                    // XXX: this pixel ->n increment is wierd
+                    (((psPixels *) rejects->data[im])->n)++;
+                }
+            }
+        }
+
+        psFree(interp);
+        psFree(maskImage);
+        psFree(maskImageF32);
+        psFree(transformedImage);
+    }
+
+    psFree(inCoords);
+    psFree(outCoords);
+    psTrace("psModules.imcombine", 3, "Exiting pmRejectPixels()\n");
+    return(rejects);
+}
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmImageCombine.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmImageCombine.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmImageCombine.h	(revision 15358)
@@ -0,0 +1,44 @@
+/* @file  pmImageCombine.h
+ *
+ * This file will perform image combination of several images of the
+ * same field, produce a list of questionable pixels, then tag some
+ * of those pixels as cosmic rays.
+ *
+ * @author Paul Price, IfA (original prototype)
+ * @author GLG, MHPCC
+ *
+ * @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-30 21:12:56 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#ifndef PM_IMAGE_COMBINE_H
+#define PM_IMAGE_COMBINE_H
+
+/// @addtogroup imcombine Image Combinations
+/// @{
+
+psImage *pmCombineImages(
+    psImage *combine,                   ///< Combined image (output)
+    psArray **questionablePixels,       ///< Array of rejection masks
+    const psArray *images,              ///< Array of input images
+    const psArray *errors,              ///< Array of input error images
+    const psArray *masks,               ///< Array of input masks
+    psU32 maskVal,                      ///< Mask value
+    const psPixels *pixels,             ///< Pixels to combine
+    psS32 numIter,                      ///< Number of rejection iterations
+    psF32 sigmaClip                     ///< Number of standard deviations at which to reject
+);
+
+psArray *pmRejectPixels(
+    const psArray *images,              ///< Array of input images
+    const psArray *masks,               ///< Array of input image masks
+    const psArray *errors,              ///< The pixels which were rejected in the combination
+    const psArray *inToOut,             ///< Transformation from input to output system
+    const psArray *outToIn,             ///< Transformation from output to input system
+    psF32 rejThreshold,                 ///< Rejection threshold
+    psF32 gradLimit                     ///< Gradient limit
+);
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmReadoutCombine.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmReadoutCombine.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmReadoutCombine.c	(revision 15358)
@@ -0,0 +1,368 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+#include "pmFPAMaskWeight.h"
+#include "pmConceptsAverage.h"
+#include "pmReadoutStack.h"
+
+#include "pmReadoutCombine.h"
+
+//#define SHOW_BUSY 1                   // Show that the function is busy
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Allocator for pmCombineParams
+pmCombineParams *pmCombineParamsAlloc(psStatsOptions combine)
+{
+    pmCombineParams *params = psAlloc(sizeof(pmCombineParams));
+
+    params->combine = combine;
+    params->maskVal = 0;
+    params->blank = 0;
+    params->nKeep = 0;
+    params->fracHigh = 0.0;
+    params->fracHigh = 0.0;
+    params->iter = 1;
+    params->rej = INFINITY;
+    params->weights = false;
+
+    return params;
+}
+
+
+// XXX: Maybe add support for S16 and S32 types.  Currently, only F32 supported.
+bool pmReadoutCombine(pmReadout *output, const psArray *inputs, const psVector *zero, const psVector *scale,
+                      const pmCombineParams *params)
+{
+    // Check inputs
+    PS_ASSERT_PTR_NON_NULL(output, false);
+    PS_ASSERT_ARRAY_NON_NULL(inputs, false);
+    PS_ASSERT_PTR_NON_NULL(params, false);
+    if (zero) {
+        PS_ASSERT_VECTOR_TYPE(zero, PS_TYPE_F32, false);
+        PS_ASSERT_VECTOR_SIZE(zero, inputs->n, false);
+    }
+    if (scale) {
+        PS_ASSERT_VECTOR_TYPE(scale, PS_TYPE_F32, false);
+        PS_ASSERT_VECTOR_SIZE(scale, inputs->n, false);
+    }
+    PS_ASSERT_FLOAT_WITHIN_RANGE(params->fracLow, 0.0, 1.0, false);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(params->fracHigh, 0.0, 1.0, false);
+    if (params->combine != PS_STAT_SAMPLE_MEAN && params->combine != PS_STAT_SAMPLE_MEDIAN &&
+            params->combine != PS_STAT_ROBUST_MEDIAN && params->combine != PS_STAT_FITTED_MEAN &&
+            params->combine != PS_STAT_CLIPPED_MEAN) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Combination method is not SAMPLE_MEAN, SAMPLE_MEDIAN, "
+                "ROBUST_MEDIAN, FITTED_MEAN or CLIPPED_MEAN.\n");
+        return false;
+    }
+    for (int i = 0; i < inputs->n; i++) {
+        pmReadout *readout = inputs->data[i]; // Readout of interest
+        if (params->weights && !readout->weight) {
+            psError(PS_ERR_UNEXPECTED_NULL, true,
+                    "Rejection based on weights requested, but no weights supplied for image %d.\n", i);
+            return false;
+        }
+    }
+
+    bool first = !output->image;        // First pass through?
+
+    pmHDU *hdu = pmHDUFromReadout(output); // Output HDU
+    if (!hdu) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find HDU for readout.\n");
+        return false;
+    }
+
+    if (first) {
+        psString comment = NULL;        // Comment to add to header
+        psStringAppend(&comment, "Combining using statistic: %x", params->combine);
+        psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, comment, "");
+        psFree(comment);
+    }
+
+    psStats *stats = psStatsAlloc(params->combine); // The statistics to use in the combination
+    if (params->combine == PS_STAT_CLIPPED_MEAN) {
+        stats->clipSigma = params->rej;
+        stats->clipIter = params->iter;
+
+        if (first) {
+            psString comment = NULL;    // Comment to add to header
+            psStringAppend(&comment, "Combination clipping: %d iterations, rejection at %f sigma",
+                           params->iter, params->rej);
+            psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, comment, "");
+            psFree(comment);
+        }
+    }
+
+    int minInputCols, maxInputCols, minInputRows, maxInputRows; // Smallest and largest values to combine
+    int xSize, ySize;                   // Size of the output image
+    if (!pmReadoutStackValidate(&minInputCols, &maxInputCols, &minInputRows, &maxInputRows, &xSize, &ySize,
+                                inputs)) {
+        psError(PS_ERR_UNKNOWN, false, "No valid input readouts.");
+        return false;
+    }
+
+    pmReadoutUpdateSize(output, minInputCols, minInputRows, xSize, ySize, true);
+    psTrace("psModules.imcombine", 7, "Output minimum: %d,%d\n", output->col0, output->row0);
+
+    psStatsOptions combineStdev = 0; // Statistics option for weights
+    if (params->weights) {
+
+        if (!output->weight) {
+            output->weight = psImageAlloc(xSize, ySize, PS_TYPE_F32);
+        }
+        if (output->weight->numCols < xSize || output->weight->numRows < ySize) {
+            psImage *newWeight = psImageAlloc(xSize, ySize, PS_TYPE_F32);
+            psImageInit(newWeight, 0.0);
+            psImageOverlaySection(newWeight, output->weight, output->col0, output->row0, "=");
+            psFree(output->weight);
+            output->weight = newWeight;
+        }
+
+        if (first) {
+            psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK,
+                             "Using input weights to combine images", "");
+        }
+
+        // Get the correct statistics option for weights
+        switch (params->combine) {
+        case PS_STAT_SAMPLE_MEAN:
+        case PS_STAT_SAMPLE_MEDIAN:
+            combineStdev = PS_STAT_SAMPLE_STDEV;
+            break;
+        case PS_STAT_ROBUST_MEDIAN:
+            combineStdev = PS_STAT_ROBUST_STDEV;
+            break;
+        case PS_STAT_FITTED_MEAN:
+            combineStdev = PS_STAT_FITTED_STDEV;
+            break;
+        case PS_STAT_CLIPPED_MEAN:
+            combineStdev = PS_STAT_CLIPPED_STDEV;
+            break;
+        default:
+            psAbort("Should never get here --- checked params->combine before.\n");
+        }
+        stats->options |= combineStdev;
+    }
+
+    // We loop through each pixel in the output image.  We loop through each input readout.  We determine if
+    // that output pixel is contained in the image from that readout.  If so, we save it in psVector pixels.
+    // If not, we set a mask for that element in pixels.  Then, we mask off pixels not between fracLow and
+    // fracHigh.  Then we call the vector stats routine on those pixels/mask.  Then we set the output pixel
+    // value to the result of the stats call.
+
+    psVector *pixels = psVectorAlloc(inputs->n, PS_TYPE_F32); // Stack of pixels
+    psF32 *pixelsData = pixels->data.F32; // Dereference pixels
+
+    psVector *mask   = psVectorAlloc(inputs->n, PS_TYPE_U8); // Mask for stack
+    psU8 *maskData = mask->data.U8;     // Dereference mask
+
+    psVector *weights = NULL;           // Stack of weights
+    psVector *errors = NULL;            // Stack of errors (sqrt of variance/weights), for psVectorStats
+    psF32 *weightsData = NULL;          // Dereference weights
+    if (params->weights) {
+        weights = psVectorAlloc(inputs->n, PS_TYPE_F32); // Stack of weights
+        weightsData = weights->data.F32;
+    }
+    psVector *index = NULL;             // The indices to sort the pixels
+
+    float keepFrac = 1.0 - params->fracLow - params->fracHigh; // Fraction of pixels to keep
+    if (keepFrac != 1.0 && first) {
+        psString comment = NULL;        // Comment to add to header
+        psStringAppend(&comment, "Min/max rejection: %f high, %f low, keep %d",
+                       params->fracHigh, params->fracLow, params->nKeep);
+        psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, comment, "");
+        psFree(comment);
+    }
+
+    psMaskType maskVal = params->maskVal; // The mask value
+    if (maskVal && first) {
+        psString comment = NULL;        // Comment to add to header
+        psStringAppend(&comment, "Mask for combination: %x", maskVal);
+        psMetadataAddStr(hdu->header, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, comment, "");
+        psFree(comment);
+    }
+
+    #ifndef PS_NO_TRACE
+    psTrace("psModules.imcombine", 3, "Iterating output: %d --> %d, %d --> %d\n",
+            minInputCols - output->col0, maxInputCols - output->col0,
+            minInputRows - output->row0, maxInputRows - output->row0);
+    if (psTraceGetLevel("psModules.imcombine") >= 3) {
+        for (int r = 0; r < inputs->n; r++) {
+            pmReadout *readout = inputs->data[r]; // Input readout
+            psTrace("psModules.imcombine", 3, "Iterating input %d: %d --> %d, %d --> %d\n", r,
+                    minInputCols - readout->col0, maxInputCols - readout->col0,
+                    minInputRows - readout->row0, maxInputRows - readout->row0);
+        }
+    }
+    #endif
+
+    // Dereference output products
+    psF32 **outputImage  = output->image->data.F32; // Output image
+    psU8  **outputMask   = output->mask->data.U8; // Output mask
+    psF32 **outputWeight = NULL; // Output weight map
+    if (output->weight) {
+        outputWeight = output->weight->data.F32;
+    }
+
+    psVector *invScale = NULL;          // Inverse scale; pre-calculated for efficiency
+    if (scale) {
+        invScale = (psVector*)psBinaryOp(NULL, psScalarAlloc(1.0, PS_TYPE_F32), "/", (const psPtr)scale);
+    }
+
+    for (int i = minInputRows; i < maxInputRows; i++) {
+        int yOut = i - output->row0; // y position on output readout
+        #ifdef SHOW_BUSY
+
+        if (psTraceGetLevel("psModules.imcombine") > 9) {
+            printf("Processing row %d\r", i);
+            fflush(stdout);
+        }
+        #endif
+        for (int j = minInputCols; j < maxInputCols; j++) {
+            int xOut = j - output->col0; // x position on output readout
+
+            int numValid = 0;           // Number of valid pixels in the stack
+            memset(maskData, 0, mask->n * sizeof(psU8)); // Reset the mask
+            for (int r = 0; r < inputs->n; r++) {
+                pmReadout *readout = inputs->data[r]; // Input readout
+                int yIn = i - readout->row0; // y position on input readout
+                int xIn = j - readout->col0; // x position on input readout
+                psImage *image = readout->image; // The readout image
+
+                #if 0 // This should have been taken care of already:
+                // Check bounds
+                if (xIn < 0 || xIn >= image->numCols || yIn < 0 || yIn >= image->numRows) {
+                    continue;
+                }
+                #endif
+
+                pixelsData[r] = image->data.F32[yIn][xIn];
+                if (!isfinite(pixelsData[r])) {
+                    maskData[r] = 1;
+                    continue;
+                }
+
+                // Check mask
+                psImage *roMask = readout->mask; // The mask image
+                if (roMask && roMask->data.U8[yIn][xIn] & maskVal) {
+                    maskData[r] = 1;
+                    continue;
+                }
+
+                if (params->weights) {
+                    weightsData[r] = readout->weight->data.F32[yIn][xIn];
+                }
+
+                if (zero) {
+                    pixelsData[r] -= zero->data.F32[r];
+                }
+                if (scale) {
+                    pixelsData[r] *= invScale->data.F32[r];
+                    if (params->weights) {
+                        weightsData[r] *= invScale->data.F32[r] * invScale->data.F32[r];
+                    }
+                }
+
+                numValid++;
+            }
+
+            if (numValid == 0) {
+                outputMask[yOut][xOut] = params->blank;
+                outputImage[yOut][xOut] = NAN;
+                continue;
+            }
+
+            // Apply fracLow,fracHigh if there are enough pixels
+            if (numValid * keepFrac >= params->nKeep && keepFrac != 1.0) {
+                index = psVectorSortIndex(index, pixels);
+                int numLow = numValid * params->fracLow; // Number of low pixels to clip
+                int numHigh = numValid * params->fracHigh; // Number of high pixels to clip
+                // Low pixels
+                psS32 *indexData = index->data.S32; // Dereference index
+                for (int k = 0, numMasked = 0; numMasked < numLow && k < index->n; k++) {
+                    // Don't count the ones that are already masked
+                    if (!maskData[indexData[k]]) {
+                        maskData[indexData[k]] = 1;
+                        numMasked++;
+                    }
+                }
+                // High pixels
+                for (int k = pixels->n - 1, numMasked = 0; numMasked < numHigh && k >= 0; k--) {
+                    // Don't count the ones that are already masked
+                    if (! maskData[indexData[k]]) {
+                        maskData[indexData[k]] = 1;
+                        numMasked++;
+                    }
+                }
+            }
+
+            // XXXXX this step probably is very expensive : convert errors to variance everywhere?
+            if (params->weights) {
+                errors = (psVector*)psUnaryOp(errors, weights, "sqrt");
+            }
+
+            // Combination
+            if (!psVectorStats(stats, pixels, errors, mask, 1)) {
+                psError(PS_ERR_UNKNOWN, false, "Error in statistics.");
+                psFree(index);
+                psFree(pixels);
+                psFree(mask);
+                psFree(weights);
+                psFree(errors);
+                psFree(stats);
+                psFree(invScale);
+                return false;
+            }
+
+            outputImage[yOut][xOut] = psStatsGetValue(stats, params->combine);
+            if (!isfinite(outputImage[yOut][xOut])) {
+                outputMask[yOut][xOut] = params->blank;
+            }
+            if (params->weights) {
+                float stdev = psStatsGetValue(stats, combineStdev);
+                outputWeight[yOut][xOut] = PS_SQR(stdev); // Variance
+                // XXXX this is not the correct formal error.
+                // also, the weighted mean is not obviously the correct thing here
+            }
+        }
+    }
+    #ifdef SHOW_BUSY
+    if (psTraceGetLevel("psModules.imcombine") > 9) {
+        printf("\n");
+    }
+    #endif
+    psFree(index);
+    psFree(pixels);
+    psFree(mask);
+    psFree(weights);
+    psFree(errors);
+    psFree(stats);
+    psFree(invScale);
+
+    // Update the "concepts"
+    psList *inputCells = psListAlloc(NULL); // List of cells
+    for (long i = 0; i < inputs->n; i++) {
+        pmReadout *readout = inputs->data[i]; // Readout of interest
+        psListAdd(inputCells, PS_LIST_TAIL, readout->parent);
+    }
+    bool success = pmConceptsAverageCells(output->parent, inputCells, NULL, NULL, true);
+    psFree(inputCells);
+
+    output->data_exists = true;
+    output->parent->data_exists = true;
+    output->parent->parent->data_exists = true;
+
+    return success;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmReadoutCombine.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmReadoutCombine.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmReadoutCombine.h	(revision 15358)
@@ -0,0 +1,49 @@
+/* @file  pmReadoutCombine.h
+ * @brief Combine multiple readouts
+ *
+ * @author George Gusciora, MHPCC
+ * @author Paul Price, IfA
+ *
+ * @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-06-02 03:51:03 $
+ * Copyright 2004-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_READOUT_COMBINE_H
+#define PM_READOUT_COMBINE_H
+
+/// @addtogroup imcombine Image Combinations
+/// @{
+
+/// Combination parameters for pmReadoutCombine.
+///
+/// These values define how the combination is performed, and should not vary by detector, so that it can be
+/// re-used for multiple combinations.
+typedef struct
+{
+    psStatsOptions combine;             ///< Statistic to use when performing the combination
+    psMaskType maskVal;                 ///< Mask value
+    psMaskType blank;                   ///< Mask value to give blank (i.e., no data) pixels
+    int nKeep;                          ///< Mimimum number of pixels to keep
+    float fracHigh;                     ///< Fraction of high pixels to immediately throw
+    float fracLow;                      ///< Fraction of low pixels to immediately throw
+    int iter;                           ///< Number of iterations for clipping (for CLIPPED_MEAN only)
+    float rej;                          ///< Rejection threshould for clipping (for CLIPPED_MEAN only)
+    bool weights;                       ///< Use the supplied weights (instead of calculated stdev)?
+}
+pmCombineParams;
+
+// Allocator for pmCombineParams
+pmCombineParams *pmCombineParamsAlloc(psStatsOptions statsOptions ///< Statistic to use for combination
+                                     );
+
+/// Combine multiple readouts, applying zero and scale, with optional minmax clipping
+bool pmReadoutCombine(pmReadout *output,///< Output readout; altered and returned
+                      const psArray *inputs,  ///< Array of input readouts (F32 image and weight, U8 mask)
+                      const psVector *zero, ///< Zero corrections to subtract from input, or NULL
+                      const psVector *scale, ///< Scale corrections to divide into input, or NULL
+                      const pmCombineParams *params ///< Combination parameters
+                     );
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmStack.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmStack.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmStack.c	(revision 15358)
@@ -0,0 +1,708 @@
+/** @file  pmStack.c
+ *
+ *  This file will perform image combination of several images of the
+ *  same field, produce a list of questionable pixels, then tag some
+ *  of those pixels as cosmic rays.
+ *
+ *  @author Paul Price, IfA
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-08-23 23:43:12 $
+ *
+ *  Copyright 2004-2007 Institute for Astronomy, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmConceptsAverage.h"
+
+#include "pmStack.h"
+
+#define PIXEL_LIST_BUFFER 100           // Number of entries to add to pixel list at a time
+#define PIXEL_MAP_BUFFER 2              // Number of entries to add to pixel map at a time
+
+// Internal values for masks
+#define MASK_BAD 0x01                   // Mask value for pixels that have formerly been masked as bad
+#define MASK_SUSPECT 0x02               // Mask value for pixels that are suspected bad (by pmStackCombine)
+
+
+// Data structure for use as a buffer when combining pixels
+// Use of this structure means we don't have to do an allocation in the combination function for each pixel
+typedef struct {
+    psVector *pixels;                   // Pixel values
+    psVector *masks;                    // Pixel masks
+    psVector *weights;                  // Pixel weights
+    psVector *sort;                     // Buffer for sorting (to get a robust estimator of the standard dev)
+} combineBuffer;
+
+static void combineBufferFree(combineBuffer *buffer)
+{
+    psFree(buffer->pixels);
+    psFree(buffer->masks);
+    psFree(buffer->weights);
+    psFree(buffer->sort);
+    return;
+}
+
+static combineBuffer *combineBufferAlloc(long numImages, // Number of images that will be combined
+                                         psStatsOptions stat // Statistic to use
+    )
+{
+    combineBuffer *buffer = psAlloc(sizeof(combineBuffer));
+    psMemSetDeallocator(buffer, (psFreeFunc)combineBufferFree);
+
+    buffer->pixels = psVectorAlloc(numImages, PS_TYPE_F32);
+    buffer->masks = psVectorAlloc(numImages, PS_TYPE_MASK);
+    buffer->weights = psVectorAlloc(numImages, PS_TYPE_F32);
+    buffer->sort = psVectorAlloc(numImages, PS_TYPE_F32);
+
+    return buffer;
+}
+
+
+// Deallocator for the stack data
+static void stackDataFree(pmStackData *data)
+{
+    psFree(data->readout);
+    psFree(data->pixels);
+    return;
+}
+
+
+// Generate a mean value for the combination
+// Not using psVectorStats because it assumes that the weights are errors, and weights by 1/error^2
+static bool combinationMean(float *mean, // Mean value, to return
+                            const psVector *values, // Values to combine
+                            const psVector *weights, // Weights to apply
+                            const psVector *masks, // Mask to apply
+                            psMaskType maskVal // Value to mask
+                            )
+{
+    assert(values && weights && masks);
+    assert(values->n == weights->n);
+    assert(values->n == masks->n);
+    assert(values->type.type == PS_TYPE_F32);
+    assert(weights->type.type == PS_TYPE_F32);
+    assert(masks->type.type == PS_TYPE_MASK);
+
+    float sumValueWeight = 0.0; // Sum of the value multiplied by the weight
+    float sumWeight = 0.0;      // Sum of the weights
+    for (int i = 0; i < values->n; i++) {
+        if (!(masks->data.PS_TYPE_MASK_DATA[i] & maskVal)) {
+            sumValueWeight += values->data.F32[i] * weights->data.F32[i];
+            sumWeight += weights->data.F32[i];
+        }
+    }
+    if (sumWeight <= 0) {
+        return false;
+    }
+
+    *mean = sumValueWeight / sumWeight;
+    return true;
+}
+
+// Generate a standard deviation for the combination
+// Not using psVectorStats because it has additional allocations which slow things down
+static bool combinationStdev(float *median, // Median value, to return
+                             float *stdev, // Standard deviation value, to return
+                             const psVector *values, // Values to combine
+                             const psVector *masks, // Mask to apply
+                             psMaskType maskVal, // Value to mask
+                             psVector *sortBuffer // Buffer for sorting
+                             )
+{
+    assert(values && masks);
+    assert(values->n == masks->n);
+    assert(values->type.type == PS_TYPE_F32);
+    assert(masks->type.type == PS_TYPE_MASK);
+    assert(sortBuffer && sortBuffer->nalloc >= values->n && sortBuffer->type.type == PS_TYPE_F32);
+
+    int num = 0;            // Number of valid values
+    for (int i = 0; i < values->n; i++) {
+        if (!(masks->data.PS_TYPE_MASK_DATA[i] & maskVal)) {
+            sortBuffer->data.F32[num++] = values->data.F32[i];
+        }
+    }
+    sortBuffer->n = num;
+    if (!psVectorSortInPlace(sortBuffer)) {
+        return false;
+    }
+    *median = num % 2 ? (sortBuffer->data.F32[num / 2] + sortBuffer->data.F32[num / 2 + 1]) / 2.0 :
+        sortBuffer->data.F32[num / 2];
+    *stdev = 0.74 * (sortBuffer->data.F32[(int)(0.75 * num)] - sortBuffer->data.F32[(int)(0.25 * num)]);
+
+    return true;
+}
+
+// Generate a variance value for the combination
+static bool combinationVariance(float *variance, // Variance value, to return
+                                const psVector *variances, // Pixel variances to combine
+                                const psVector *weights, // Image weights to apply
+                                const psVector *masks, // Mask to apply
+                                psMaskType maskVal // Value to mask
+                                )
+{
+    assert(variances && weights && masks);
+    assert(variances->n == weights->n);
+    assert(variances->n == masks->n);
+    assert(variances->type.type == PS_TYPE_F32);
+    assert(weights->type.type == PS_TYPE_F32);
+    assert(masks->type.type == PS_TYPE_MASK);
+
+    // Get the variance in the combination.  We're not using the input pixel variances to generate a
+    // weighted average for the pixel flux (because that introduces systematic biases), so the variance
+    // of the output pixel value should simply be:
+    //     simga^2 = sum(weight_i^2 * sigma_i^2) / (sum(weight_i))^2
+    // This reduces, when the weights are all identically unity, to:
+    //     variance_combination = sum(variance_i) / N^2
+    // and if the variances are all equal:
+    //     variance_combination = variance_individual / N
+    // which makes sense --- the standard deviation of the combination is reduced by a factor of sqrt(N).
+
+    float sumWeights = 0.0;             // Sum of the global weights
+    float sumVarianceWeights = 0.0;     // Sum of the pixel variances multiplied by the global weights
+    for (int i = 0; i < variances->n; i++) {
+        if (!(masks->data.PS_TYPE_MASK_DATA[i] & maskVal)) {
+            sumVarianceWeights += variances->data.F32[i] * PS_SQR(weights->data.F32[i]);
+            sumWeights += weights->data.F32[i];
+        }
+    }
+
+    if (sumWeights <= 0) {
+        return false;
+    }
+
+    *variance = sumVarianceWeights / PS_SQR(sumWeights);
+    return true;
+}
+
+// Given a stack of images, combine with optional rejection.
+// Pixels in the stack that are rejected are marked for subsequent
+static bool combinePixels(psImage *image, // Combined image, for output
+                          psImage *mask, // Combined mask, for output
+                          psImage *weight, // Combined variance map, for output
+                          const psArray *inputs, // Stack data
+                          const psVector *weights, // Global (single value) weights for data, or NULL
+                          const psVector *reject, // Indices of pixels to reject, or NULL
+                          int x, int y, // Coordinates of interest
+                          psMaskType maskVal, // Value to mask
+                          psMaskType bad, // Value to give bad pixels
+                          int numIter, // Number of rejection iterations
+                          float rej, // Number of standard deviations at which to reject
+                          combineBuffer *buffer // Buffer for combination; to avoid multiple allocations
+                         )
+{
+    // Rudimentary error checking
+    assert(image);
+    assert(mask);
+    assert(inputs);
+    assert(numIter >= 0);
+    assert(rej > 0);
+    assert(buffer);
+
+    int num = inputs->n;          // Number of images to combine
+
+    psVector *pixelData = buffer->pixels; // Values for the pixel of interest
+    psVector *pixelMasks = buffer->masks; // Masks for the pixel of interest
+    psVector *pixelWeights = buffer->weights; // Weights for the pixel of interest
+    psVector *sort = buffer->sort;      // Sort buffer
+
+    // Extract the pixel and mask data
+    int numBad = 0;                     // Number of bad pixels
+    for (int i = 0; i < num; i++) {
+        pmStackData *data = inputs->data[i]; // Stack data of interest
+        psImage *image = data->readout->image; // Image of interest
+        psImage *weight = data->readout->weight; // Weight map of interest
+        psImage *mask = data->readout->mask; // Mask of interest
+        pixelData->data.F32[i] = image->data.F32[y][x];
+        if (weight) {
+            pixelWeights->data.F32[i] = weight->data.F32[y][x];
+        }
+        pixelMasks->data.PS_TYPE_MASK_DATA[i] = mask->data.PS_TYPE_MASK_DATA[y][x];
+        if (pixelMasks->data.PS_TYPE_MASK_DATA[i] & maskVal) {
+            numBad++;
+        }
+    }
+    if (numBad == num) {
+        // Catch the case where everything is masked
+        image->data.F32[y][x] = NAN;
+        if (weight) {
+            weight->data.F32[y][x] = NAN;
+        }
+        mask->data.PS_TYPE_MASK_DATA[y][x] = bad;
+        return false;
+    }
+
+    if (reject) {
+        for (int i = 0; i < reject->n; i++) {
+            pixelMasks->data.PS_TYPE_MASK_DATA[reject->data.U16[i]] |= maskVal;
+        }
+    }
+
+    // Record the value derived with no clipping, because pixels rejected using the harsh clipping applied in
+    // the first pass might later be accepted.
+    float mean, variance;               // Mean and variance of the combination
+    if (!combinationMean(&mean, pixelData, weights, pixelMasks, maskVal) ||
+        (weight && !combinationVariance(&variance, pixelWeights, weights, pixelMasks, maskVal))) {
+        image->data.F32[y][x] = NAN;
+            mask->data.PS_TYPE_MASK_DATA[y][x] = bad;
+        if (weight) {
+            weight->data.F32[y][x] = NAN;
+        }
+        return false;
+    }
+
+    image->data.F32[y][x] = mean;
+    if (weight) {
+        weight->data.F32[y][x] = variance;
+    }
+    mask->data.PS_TYPE_MASK_DATA[y][x] = 0;
+
+    // The clipping that follows is solely to identify suspect pixels.
+    // These suspect pixels will be inspected in more detail by other functions.
+    long numClipped = LONG_MAX;         // Number of pixels clipped
+    psMaskType ignore = maskVal | bad;  // Ignore values with this mask value
+    for (int i = 0; i < numIter && numClipped > 0; i++) {
+        float median, stdev;                    // Median and stdev of the combination, for rejection
+        // Only get the mean if it's not the first iteration (because we got the mean earlier)
+        if (!combinationStdev(&median, &stdev, pixelData, pixelMasks, maskVal, sort)) {
+            image->data.F32[y][x] = NAN;
+            mask->data.PS_TYPE_MASK_DATA[y][x] = bad;
+            weight->data.F32[y][x] = NAN;
+            return false;
+        }
+
+        float limit = rej * stdev; // Rejection limit
+        numClipped = 0;
+        for (int j = 0; j < num; j++) {
+            if (pixelMasks->data.PS_TYPE_MASK_DATA[j] & ignore) {
+                continue;
+            }
+            float diff = pixelData->data.F32[j] - median;
+            if (fabsf(diff) > limit) {
+                // Add the pixel as one to inspect
+                pmStackData *data = inputs->data[j]; // Stack data of interest
+                data->pixels = psPixelsAdd(data->pixels, PIXEL_LIST_BUFFER, x, y);
+                // Mask it so it's not considered in other iterations within this function
+                pixelMasks->data.PS_TYPE_MASK_DATA[j] |= bad;
+                numClipped++;
+            }
+        }
+    }
+
+    return true;
+}
+
+
+// Ensure the input array of pmStackData is valid, and get some details out of it
+static bool validateInputData(bool *haveWeights, // Do we have weights in the sky images?
+                              bool *havePixels, // Do we have lists of pixels?
+                              int *num,    // Number of inputs
+                              int *numCols, int *numRows, // Size of (sky) images
+                              psArray *input // Input array of pmStackData to validate
+    )
+{
+    PS_ASSERT_ARRAY_NON_NULL(input, false);
+    *num = input->n;
+
+    // The first is a template
+    pmStackData *data = input->data[0]; // First image off the rank
+    PS_ASSERT_PTR_NON_NULL(data, false);
+    assert(psMemGetDeallocator(data) == (psFreeFunc)stackDataFree); // Ensure it's the right type
+    *haveWeights = false;
+    PS_ASSERT_IMAGE_NON_NULL(data->readout->image, false);
+    PS_ASSERT_IMAGE_TYPE(data->readout->image, PS_TYPE_F32, false);
+    PS_ASSERT_IMAGE_NON_NULL(data->readout->mask, false);
+    PS_ASSERT_IMAGE_TYPE(data->readout->mask, PS_TYPE_MASK, false);
+    PS_ASSERT_IMAGES_SIZE_EQUAL(data->readout->image, data->readout->mask, false);
+    *numCols = data->readout->image->numCols;
+    *numRows = data->readout->image->numRows;
+    if (data->readout->weight) {
+        *haveWeights = true;
+        PS_ASSERT_IMAGE_NON_NULL(data->readout->weight, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(data->readout->image, data->readout->weight, false);
+        PS_ASSERT_IMAGE_TYPE(data->readout->weight, PS_TYPE_F32, false);
+    }
+    *havePixels = (data->pixels != NULL);
+
+    // Make sure the rest correspond with the first
+    for (int i = 1; i < *num; i++) {
+        pmStackData *data = input->data[i]; // Stack data for this input
+        assert(psMemGetDeallocator(data) == (psFreeFunc)stackDataFree); // Ensure it's the right type
+        if (!data->readout) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "The readout is specified in some but not all inputs.");
+            return false;
+        }
+        if ((*havePixels && !data->pixels) || (data->pixels && !*havePixels)) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "The pixels are specified in some but not all inputs.");
+            return false;
+        }
+        PS_ASSERT_IMAGE_NON_NULL(data->readout->image, false);
+        PS_ASSERT_IMAGE_NON_NULL(data->readout->mask, false);
+        PS_ASSERT_IMAGE_TYPE(data->readout->image, PS_TYPE_F32, false);
+        PS_ASSERT_IMAGE_TYPE(data->readout->mask, PS_TYPE_MASK, false);
+        PS_ASSERT_IMAGE_SIZE(data->readout->image, *numCols, *numRows, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(data->readout->image, data->readout->mask, false);
+        if (*haveWeights) {
+            PS_ASSERT_IMAGE_NON_NULL(data->readout->weight, false);
+            PS_ASSERT_IMAGES_SIZE_EQUAL(data->readout->image, data->readout->weight, false);
+            PS_ASSERT_IMAGE_TYPE(data->readout->weight, PS_TYPE_F32, false);
+        }
+    }
+
+    return true;
+}
+
+
+#if 0
+// Examine a pixel carefully to see if it should be rejected in the stack by convolving all the input images
+// to the same seeing, and clipping there.
+static bool convolveTest(psVector *reject, // Image indices to reject for this pixel (output)
+                         psVector *inspect, // Images indices to inspect for this pixel (input)
+                         psArray *input, // The input pmStackData
+                         int xPix, int yPix,   // Pixel coordinates of interest
+                         psVector *seeing, // Seeing convolution for each image
+                         psMaskType maskVal, // Value to mask
+                         float extent,   // Extent of convolution (Gaussian sigmas)
+                         float threshold// Rejection threshold (standard deviations)
+    )
+{
+    assert(reject && reject->type.type == PS_TYPE_U16);
+    assert(input);
+    assert(seeing);
+    assert(seeing->n == input->n);
+
+    int num = input->n;                // Number of input images
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV); // Statistics
+    psVector *values = psVectorAlloc(num, PS_TYPE_F32); // Values from convolving the image
+    psVector *valuesMask = psVectorAlloc(num, PS_TYPE_MASK); // Mask for convolution values
+
+    for (int i = 0; i < num; i++) {
+        pmStackData *data = input->data[i]; // Stacking data
+        int radius = extent * seeing->data.F32[i]; // How much to convolve
+        psImage *image = data->readout->image; // Image to convolve
+        psImage *mask = data->readout->mask; // Image mask
+
+        int xMin = PS_MAX(xPix - radius, 0);
+        int xMax = PS_MIN(xPix + radius, image->numCols - 1);
+        int yMin = PS_MAX(yPix - radius, 0);
+        int yMax = PS_MIN(yPix + radius, image->numRows - 1);
+
+        float sum = 0.0;            // Sum of pixels in convolution
+        float sumKernel = 0.0;      // Sum of kernel
+        for (int y = yMin; y <= yMax; y++) {
+            int v2 = PS_SQR(y - yPix);
+            for (int x = xMin; x <= xMax; x++) {
+                int u2 = PS_SQR(x - xPix);
+                float kernel = expf(-(u2 + v2) / seeing->data.F32[i]);
+                if (!(mask->data.PS_TYPE_MASK_DATA[y][x] & maskVal)) {
+                    sum += image->data.F32[y][x] * kernel;
+                    sumKernel += kernel;
+                }
+            }
+        }
+
+        if (sumKernel != 0.0) {
+            values->data.F32[i] = sum / sumKernel;
+            valuesMask->data.PS_TYPE_MASK_DATA[i] = 0;
+        } else {
+            // Everything's masked out; don't include this pixel in the analysis
+            values->data.F32[i] = NAN;
+            valuesMask->data.PS_TYPE_MASK_DATA[i] = MASK_BAD;
+        }
+    }
+
+    // Mask the ones we're inspecting
+    for (int i = 0; i < inspect->n; i++) {
+        valuesMask->data.PS_TYPE_MASK_DATA[inspect->data.U16[i]] |= MASK_SUSPECT;
+    }
+
+    if (!psVectorStats(stats, values, NULL, valuesMask, MASK_BAD | MASK_SUSPECT)) {
+        psFree(stats);
+        psFree(values);
+        psFree(valuesMask);
+        return false;
+    }
+
+    // Inspect the pixels of interest
+    float mean = stats->sampleMean;       // Mean value
+    float limit = threshold * stats->sampleStdev; // Rejection threshold
+    for (int j = 0; j < inspect->n; j++) {
+        int index = inspect->data.U16[j]; // Index of interest
+        if (fabsf(values->data.F32[index] - mean) > limit) {
+            reject->data.U16[reject->n++] = index;
+        }
+    }
+
+    psFree(stats);
+    psFree(values);
+    psFree(valuesMask);
+
+    return true;
+}
+#endif
+
+
+// Generate a "pixel map".
+//
+// A "pixel map" is an image-like structure containing a vector that contains the indices of images.  The idea
+// is to provide a reverse lookup for an array of pixel lists, so that the image for which a pixel is flagged
+// can be identified easily.
+static psArray *pixelMapGenerate(const psArray *input, // Data to stack
+                                 int numCols, int numRows // Size of (sky) images
+    )
+{
+    psArray *map = psArrayAlloc(numRows); // The pixel map
+    for (int y = 0; y < numRows; y++) {
+        map->data[y] = psArrayAlloc(numCols);
+    }
+
+    for (int i = 0; i < input->n; i++) {
+        pmStackData *data = input->data[i];
+        assert(data && data->pixels);
+        psPixels *pixels = data->pixels;// The pixels of interest
+        for (int j = 0; j < pixels->n; j++) {
+            int x = pixels->data[j].x, y = pixels->data[j].y; // Coordinates of interest
+            psArray *columns = map->data[y]; // The columns for that row
+            psVector *images = columns->data[x]; // The images for that column
+            if (!images) {
+                images = columns->data[x] = psVectorAllocEmpty(PIXEL_MAP_BUFFER, PS_TYPE_U16);
+            }
+            int size = images->n;       // Element number at which to add
+            columns->data[x] = psVectorExtend(images, PIXEL_MAP_BUFFER, 1);
+            images->data.U16[size] = i;
+        }
+    }
+
+    return map;
+}
+
+// Query a "pixel map", by returning the list of image indices for a particular pixel.
+static psVector *pixelMapQuery(const psArray *map, // Pixel map
+                               int x, int y // Coordinates of interest
+    )
+{
+    assert(y >= 0 && y < map->n);
+    psArray *colMap = map->data[y];     // Columns for that row
+    assert(x >= 0 && x < colMap->n);
+    return colMap->data[x];
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/// Constructor
+pmStackData *pmStackDataAlloc(pmReadout *readout, float weight)
+{
+    pmStackData *data = psAlloc(sizeof(pmStackData)); // Stack data, to return
+    psMemSetDeallocator(data, (psFreeFunc)stackDataFree);
+
+    data->readout = psMemIncrRefCounter(readout);
+    data->pixels = NULL;
+    data->weight = weight;
+
+    return data;
+}
+
+/// Stack input images
+bool pmStackCombine(pmReadout *combined, psArray *input, psMaskType maskVal, psMaskType bad,
+                    int numIter, float rej)
+{
+    PS_ASSERT_PTR_NON_NULL(combined, false);
+    bool haveWeights;                   // Do we have the weight maps?
+    bool havePixels;                    // Do we have lists of pixels?
+    int num;                            // Number of inputs
+    int numCols, numRows;               // Size of (sky) images
+    if (!validateInputData(&haveWeights, &havePixels, &num, &numCols, &numRows, input)) {
+        return false;
+    }
+    PS_ASSERT_INT_POSITIVE(bad, false);
+    PS_ASSERT_INT_NONNEGATIVE(numIter, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, false);
+    if (havePixels) {
+        // This is a subsequent combination, so expect that the image and mask already exist
+        PS_ASSERT_IMAGE_NON_NULL(combined->image, false);
+        PS_ASSERT_IMAGE_TYPE(combined->image, PS_TYPE_F32, false);
+        PS_ASSERT_IMAGE_NON_NULL(combined->mask, false);
+        PS_ASSERT_IMAGE_TYPE(combined->mask, PS_TYPE_MASK, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(combined->image, combined->mask, false);
+    }
+
+    // Pull out the weightings
+    psVector *weights = psVectorAlloc(num, PS_TYPE_F32);
+    for (int i = 0; i < num; i++) {
+        pmStackData *data = input->data[i]; // Stack data for this input
+        weights->data.F32[i] = data->weight;
+    }
+
+    // Buffer for combination
+    combineBuffer *buffer = combineBufferAlloc(num, numIter == 0 ? PS_STAT_SAMPLE_MEAN :
+                                               PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_MEDIAN);
+
+    if (havePixels) {
+        // Only combine select pixels
+        psImage *combinedImage = combined->image; // Combined image
+        psImage *combinedMask = combined->mask; // Combined mask
+        psImage *combinedWeight = combined->weight; // Combined mask
+
+        psArray *pixelMap = pixelMapGenerate(input, numCols, numRows); // Map of pixels to source
+        psPixels *pixels = NULL;            // Total list of pixels, with no duplicates
+        for (int i = 0; i < num; i++) {
+            pmStackData *data = input->data[i]; // Stacking data; contains the list of pixels
+            pixels = psPixelsConcatenate(pixels, data->pixels);
+            data->pixels = psPixelsRealloc(data->pixels, PIXEL_LIST_BUFFER); // Just in case more rejection
+        }
+        for (int i = 0; i < pixels->n; i++) {
+            int x = pixels->data[i].x, y = pixels->data[i].y; // Coordinates of interest
+            psVector *reject = pixelMapQuery(pixelMap, x, y); // Inspect these images closely
+            combinePixels(combinedImage, combinedMask, combinedWeight, input, weights, reject, x, y,
+                          maskVal, bad, numIter, rej, buffer);
+        }
+        psTrace("psModules.imcombine", 5, "Additional %ld pixels fixed.\n", pixels->n);
+        psFree(pixels);
+        psFree(pixelMap);
+    } else {
+        // Pull the products out, allocate if necessary
+        psImage *combinedImage = combined->image; // Combined image
+        if (!combinedImage) {
+            combined->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+            combinedImage = combined->image;
+        }
+        psImage *combinedMask = combined->mask; // Combined mask
+        if (!combinedMask) {
+            combined->mask = psImageAlloc(numCols, numRows, PS_TYPE_MASK);
+            combinedMask = combined->mask;
+        }
+
+        psImage *combinedWeights = combined->weight; // Combined weight map
+        if (haveWeights && !combinedWeights) {
+            combined->weight = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+            combinedWeights = combined->weight;
+        }
+
+        // Generate the pixel lists in which to place the rejected pixels
+        if (numIter != 0) {
+            for (int i = 0; i < num; i++) {
+                pmStackData *data = input->data[i]; // Stack data for this input
+                data->pixels = psPixelsAllocEmpty(PIXEL_LIST_BUFFER);
+            }
+        }
+
+        for (int y = 0; y < numRows; y++) {
+            for (int x = 0; x < numCols; x++) {
+                combinePixels(combinedImage, combinedMask, combinedWeights, input, weights, NULL, x, y,
+                              maskVal, bad, numIter, rej, buffer);
+            }
+        }
+
+#ifndef PS_NO_TRACE
+        if (psTraceGetLevel("psModules.imcombine") >= 5) {
+            for (int i = 0; i < num; i++) {
+                pmStackData *data = input->data[i]; // Stack data for this input
+                psTrace("psModules.imcombine", 5, "Image %d: %ld pixels to inspect.\n", i, data->pixels->n);
+            }
+        }
+#endif
+    }
+
+    psFree(weights);
+    psFree(buffer);
+
+    return true;
+}
+
+#if 0
+bool pmStackReject(psArray *input, psMaskType maskVal, float extent, float threshold)
+{
+    bool haveWeights;                   // Do we have the sky weight maps?
+    bool havePixels;                    // Do we have lists of pixels?
+    int num;                            // Number of inputs
+    int numCols, numRows;               // Size of (sky) images
+    if (!validateInputData(&haveWeights, &havePixels, &num, &numCols, &numRows, input)) {
+        return false;
+    }
+    if (!havePixels) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "No pixels have been flagged for possible rejection.");
+        return false;
+    }
+    PS_ASSERT_FLOAT_LARGER_THAN(extent, 0.0, false);
+
+    // Want to concatenate the list of pixels, but also want to know where each came from
+    psArray *pixelMap = pixelMapGenerate(input, numCols, numRows); // Map of pixels to source
+    psPixels *pixels = NULL;            // Total list of pixels, with no duplicates
+    for (int i = 0; i < num; i++) {
+        pmStackData *data = input->data[i]; // Stacking data; contains the list of pixels
+        pixels = psPixelsConcatenate(pixels, data->pixels);
+        // Throw out the old in preparation for the new
+        data->pixels = psPixelsRealloc(data->pixels, 0);
+    }
+
+    // Get the convolution widths
+    psVector *seeing = psVectorAlloc(num, PS_TYPE_F32); // Seeing for each image
+    const float fwhm2sigma = 1.0 / (2.0*sqrt((double)2.0*(log((double)2.0)))); // Conversion FWHM --> sigma
+    float seeingMax = -INFINITY;                // Maximum FWHM
+    for (int i = 0; i < num; i++) {
+        pmStackData *data = input->data[i]; // Stacking data
+        seeing->data.F32[i] = data->seeing * fwhm2sigma;
+        if (data->seeing > seeingMax) {
+            seeingMax = data->seeing;
+        }
+    }
+    (void)psBinaryOp(seeing, seeing, "*", seeing);
+    (void)psBinaryOp(seeing, psScalarAlloc(PS_SQR(seeingMax), PS_TYPE_F32), "-", seeing);
+    (void)psUnaryOp(seeing, seeing, "sqrt");
+
+
+    psVector *reject = psVectorAllocEmpty(num, PS_TYPE_U16); // Image indices to reject for a given pixel
+
+    for (int i = 0; i < pixels->n; i++) {
+        int x = pixels->data[i].x;      // x coordinate of interest
+        int y = pixels->data[i].y;      // y coordinate of interest
+        psVector *inspect = pixelMapQuery(pixelMap, x, y); // Inspect these images closely
+
+        // Can daisy-chain multiple tests here
+        reject->n = 0;
+
+        if (!convolveTest(reject, inspect, input, x, y, seeing, maskVal, extent, threshold)) {
+            // Some sort of error message here; NAN and mask all the pixels
+            for (int i = 0; i < num; i++) {
+                pmStackData *data = input->data[i]; // Stacking data
+                psPixelsAdd(data->pixels, PIXEL_LIST_BUFFER, x, y);
+            }
+            continue;
+        }
+
+        // Add the rejected pixels to the official rejection list
+        for (int i = 0; i < reject->n; i++) {
+            pmStackData *data = input->data[reject->data.U16[i]]; // Stacking data
+            psPixelsAdd(data->pixels, PIXEL_LIST_BUFFER, x, y);
+        }
+    }
+    psFree(reject);
+    psFree(seeing);
+    psFree(pixels);
+    psFree(pixelMap);
+
+#ifndef PS_NO_TRACE
+    if (psTraceGetLevel("psModules.imcombine") >= 5) {
+        for (int i = 0; i < num; i++) {
+            pmStackData *data = input->data[i]; // Stack data for this input
+            psTrace("psModules.imcombine", 5, "Image %d: %ld pixels rejected.\n", i, data->pixels->n);
+        }
+    }
+#endif
+
+    return true;
+}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmStack.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmStack.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmStack.h	(revision 15358)
@@ -0,0 +1,83 @@
+/* @file  pmStack.h
+ *
+ * This file will perform image combination of several images of the
+ * same field, produce a list of questionable pixels, then tag some
+ * of those pixels as defects.
+ *
+ * @author Paul Price, IfA
+ * @author GLG, MHPCC
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-08-23 23:43:12 $
+ *
+ * Copyright 2004-2007 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_STACK_H
+#define PM_STACK_H
+
+#include <pslib.h>
+#include <pmHDU.h>
+#include <pmFPA.h>
+
+/// @addtogroup imcombine Image Combinations
+/// @{
+
+
+/// Container for input image
+typedef struct {
+    pmReadout *readout;                 ///< Warped readout (sky cell)
+    psPixels *pixels;                   ///< Pixels to inspect or reject
+    float weight;                       ///< Weight to apply
+} pmStackData;
+
+/// Constructor
+pmStackData *pmStackDataAlloc(pmReadout *readout, ///< Warped readout (sky cell)
+                              float weight ///< Weight to apply
+    );
+
+/// Stack input images
+bool pmStackCombine(pmReadout *combined,///< Combined readout (output)
+                    psArray *input,     ///< Input array of pmStackData
+                    psMaskType maskVal, ///< Mask value of bad pixels
+                    psMaskType bad,     ///< Mask value to give rejected pixels
+                    int numIter,        ///< Number of iterations
+                    float rej           ///< Rejection limit (standard deviations)
+    );
+
+#if 0
+psImage *pmStackCombine(psImage *combine, ///< Combined image (output)
+                        psArray **questionablePixels, ///< Array of rejection masks
+                        const psArray *images, ///< Array of input images
+                        const psArray *errors, ///< Array of input error images
+                        const psArray *masks, ///< Array of input masks
+                        psU32 maskVal,  ///< Mask value
+                        const psPixels *pixels, ///< Pixels to combine
+                        psS32 numIter,  ///< Number of rejection iterations
+                        psF32 sigmaClip ///< Number of standard deviations at which to reject
+);
+#endif
+
+#if 0
+/// Reject pixles in input images
+bool pmStackReject(psArray *input,      ///< Input array of pmStackData
+                   psMaskType maskVal,  ///< Value to mask
+                   float extent,        ///< Gaussian convolution extent, in Gaussian sigmas
+                   float threshold      ///< Rejection threshold, in standard deviations
+                   );
+#endif
+
+#if 0
+psArray *pmStackReject(
+    const psArray *images,              ///< Array of input images
+    const psArray *masks,               ///< Array of input image masks
+    const psArray *errors,              ///< The pixels which were rejected in the combination
+    const psArray *inToOut,             ///< Transformation from input to output system
+    const psArray *outToIn,             ///< Transformation from output to input system
+    psF32 rejThreshold,                 ///< Rejection threshold
+    psF32 gradLimit                     ///< Gradient limit
+);
+#endif
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmStackReject.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmStackReject.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmStackReject.c	(revision 15358)
@@ -0,0 +1,124 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmSubtraction.h"
+#include "pmSubtractionKernels.h"
+
+#define PIXEL_LIST_BUFFER 100           // Number of pixels to add to list at a time
+
+psPixels *pmStackReject(const psPixels *in, float threshold, const psArray *regions,
+                        const psArray *solutions, const pmSubtractionKernels *kernels)
+{
+    PS_ASSERT_PIXELS_NON_NULL(in, NULL);
+    PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(threshold, 0.0, NULL);
+    PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(threshold, 1.0, NULL);
+    PS_ASSERT_ARRAY_NON_NULL(regions, NULL);
+    PS_ASSERT_ARRAY_NON_NULL(solutions, NULL);
+    PS_ASSERT_ARRAYS_SIZE_EQUAL(regions, solutions, NULL);
+    PS_ASSERT_PTR_NON_NULL(kernels, NULL);
+
+    // Get the original image size
+    int numRegions = regions->n;        // Number of regions
+    int numCols = 0, numRows = 0;       // Size of original image
+    int minCols = INT_MAX, minRows = INT_MAX; // Minimum coordinate for image --- should be 0,0
+    for (int i = 0; i < numRegions; i++) {
+        psRegion *region = regions->data[i]; // Region of interest
+        if (region->x0 < minCols) {
+            minCols = region->x0;
+        }
+        if (region->y0 < minRows) {
+            minRows = region->y0;
+        }
+        if (region->x1 > numCols) {
+            numCols = region->x1;
+        }
+        if (region->y1 > numRows) {
+            numRows = region->y1;
+        }
+    }
+    if (minCols != 0 || minRows != 0) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Some error with image regions --- minimum coordinate is not 0,0");
+        return NULL;
+    }
+
+    psImage *mask = psPixelsToMask(NULL, in, psRegionSet(0, numCols, 0, numRows), 0x01); // Mask image
+    psImage *image = psImageCopy(NULL, mask, PS_TYPE_F32); // Floating-point version, so we can convolve
+    psFree(mask);
+
+    // Convolve the image with the kernel --- we're basically applying a matched filter and then thresholding
+    psImage *convolved = NULL;          // Convolved image
+    for (int i = 0; i < numRegions; i++) {
+        psRegion *region = regions->data[i]; // Region of interest
+        psVector *solution = solutions->data[i]; // Solution of interest
+        if (!pmSubtractionConvolve(&convolved, NULL, NULL, image, NULL, NULL, 0,
+                                   region, solution, kernels, true)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to convolve mask image in region %d.", i);
+            psFree(convolved);
+            psFree(image);
+            return NULL;
+        }
+
+        // Need to adjust the thresholding level for the normalisation of the kernel --- the application of
+        // the kernel may scale the unit level that we've inserted.
+
+        // Image of the kernel at the centre of the region
+        float xNorm = (region->x0 + 0.5 * (region->x1 - region->x0) - numCols/2.0) / (float)numCols;
+        float yNorm = (region->y0 + 0.5 * (region->y1 - region->y0) - numRows/2.0) / (float)numRows;
+        psImage *kernel = pmSubtractionKernelImage(solution, kernels, xNorm, yNorm);
+        if (!kernel) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to generate kernel image.");
+            psFree(convolved);
+            psFree(image);
+            return NULL;
+        }
+        float sum = 0.0;
+        for (int y = 0; y < kernel->numRows; y++) {
+            for (int x = 0; x < kernel->numCols; x++) {
+                sum += kernel->data.F32[y][x];
+            }
+        }
+        psFree(kernel);
+
+        psImage *subConv = psImageSubset(convolved, *region); // Sub-image of convolved image
+        psBinaryOp(subConv, subConv, "*", psScalarAlloc(1.0 / sum, PS_TYPE_F32));
+    }
+    psFree(image);
+
+    // Threshold the convolved image
+    psPixels *bad = psPixelsAllocEmpty(PIXEL_LIST_BUFFER); // List of pixels that should be masked
+    for (int y = 0; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            if (convolved->data.F32[y][x] > threshold) {
+                bad = psPixelsAdd(bad, PIXEL_LIST_BUFFER, x, y);
+            }
+        }
+    }
+    psFree(convolved);
+
+    // Now, we want to convolve the original pixels properly
+    mask = psPixelsToMask(NULL, bad, psRegionSet(0, numCols, 0, numRows), 0xff);
+    int size = kernels->size;           // Size of kernels
+    for (int i = 0; i < bad->n; i++) {
+        int xPix = bad->data[i].x, yPix = bad->data[i].y; // Coordinates of interest
+        // Convolution limits
+        int xMin = PS_MAX(xPix - size, 0);
+        int xMax = PS_MIN(xPix + size, numCols - 1);
+        int yMin = PS_MAX(yPix - size, 0);
+        int yMax = PS_MIN(yPix + size, numRows - 1);
+        for (int y = yMin; y <= yMax; y++) {
+            for (int x = xMin; x <= xMax; x++) {
+                mask->data.PS_TYPE_MASK_DATA[y][x] = 0xff;
+            }
+        }
+    }
+
+    bad = psPixelsFromMask(bad, mask, 0xff);
+    psFree(mask);
+
+    return bad;
+}
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmStackReject.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmStackReject.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmStackReject.h	(revision 15358)
@@ -0,0 +1,19 @@
+#ifndef PM_STACK_REJECT_H
+#define PM_STACK_REJECT_H
+
+#include <pslib.h>
+#include <pmSubtractionKernels.h>
+
+/// Given a list of pixels from the convolved image, find the corresponding (smaller subset of) pixels in the
+/// original image, and then convolve those to get the list of all pixels which should be rejected
+///
+/// We apply a matched filter to the corresponding mask image, and threshold to find the original pixels
+psPixels *pmStackReject(const psPixels *in, ///< List of pixels in the convolved image
+                        float threshold, ///< Threshold on convolved image, 0..1
+                        const psArray *regions, ///< Array of image regions for image
+                        const psArray *solutions, ///< Array of solution vectors for image
+                        const pmSubtractionKernels *kernels ///< Kernel parameters
+    );
+
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtraction.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtraction.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtraction.c	(revision 15358)
@@ -0,0 +1,1237 @@
+/** @file pmSubtraction.c
+ *
+ *  @author Paul Price, IfA
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.65 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-10-17 02:45:40 $
+ *
+ *  Copyright 2004-2007 Institute for Astronomy, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"                      // Required for pmFPA.h
+#include "pmFPA.h"
+#include "pmSubtractionStamps.h"
+
+#include "pmSubtraction.h"
+
+//#define TESTING
+
+#define PIXEL_LIST_BUFFER 100           // Number of entries to add to pixel list at a time
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Private (file-static) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Return the number of polynomial terms there are for a given order
+static inline long polyTerms(int order  // Order of polynomial
+                             )
+{
+    return (order + 1) * (order + 2) / 2;
+}
+
+// Given a stamp coordinates (x,y), generate a matrix where the elements (i,j) are x^i * y^j
+static psImage *spatialPolyValues(int spatialOrder, // Maximum spatial polynomial order
+                                  float x, float y // Normalised position of interest, [-1,1]
+    )
+{
+    assert(spatialOrder >= 0);
+    assert(x >= -1 && x <= 1);
+    assert(y >= -1 && y <= 1);
+
+    psImage *polyValues = psImageAlloc(spatialOrder + 1, spatialOrder + 1, PS_TYPE_F64); // Matrix to return
+    polyValues->data.F64[0][0] = 1.0;
+
+    double value = 1.0;
+    for (int i = 1; i <= spatialOrder; i++) {
+        value *= x;
+        polyValues->data.F64[0][i] = value;
+    }
+
+    value = 1.0;
+    for (int j = 1; j <= spatialOrder; j++) {
+        value *= y;
+        polyValues->data.F64[j][0] = value;
+    }
+
+    for (int j = 1; j <= spatialOrder; j++) {
+        for (int i = 1; i <= spatialOrder - j; i++) {
+            polyValues->data.F64[j][i] = polyValues->data.F64[j][0] * polyValues->data.F64[0][i];
+        }
+    }
+
+    return polyValues;
+}
+
+// Generate the kernel to apply to the variance from the normal kernel
+static psKernel *varianceKernel(psKernel *normalKernel // Normal kernel
+                                )
+{
+    // Kernel range
+    int xMin = normalKernel->xMin, xMax = normalKernel->xMax;
+    int yMin = normalKernel->yMin, yMax = normalKernel->yMax;
+
+    psKernel *kernel = psKernelAlloc(xMin, xMax, yMin, yMax); // Kernel to return
+
+    // Take the square of the normal kernel
+    double sumNormal = 0.0, sumVariance = 0.0; // Sum of the normal and variance kernels
+    for (int v = yMin; v <= yMax; v++) {
+        for (int u = xMin; u <= xMax; u++) {
+            float value = normalKernel->kernel[v][u]; // Value of interest
+            float value2 = PS_SQR(value); // Value squared
+            sumNormal += value;
+            sumVariance += value2;
+            kernel->kernel[v][u] = value2;
+        }
+    }
+
+    // Normalise so that the sum of the variance kernel is the square of the sum of the normal kernel
+    // This is required to keep the relative scaling between the image and the weight map
+    psBinaryOp(kernel->image, kernel->image, "*",
+               psScalarAlloc(PS_SQR(sumNormal) / sumVariance, PS_TYPE_F32));
+
+    return kernel;
+}
+
+// Generate an image of the solved kernel
+static inline psKernel *solvedKernel(psKernel *kernel, // Kernel, to return
+                                     const psVector *solution, // Solution to the least-squares problem
+                                     const pmSubtractionKernels *kernels, // Kernel basis functions
+                                     const psImage *polyValues // Spatial polynomial values
+                                     )
+{
+    int numKernels = kernels->num;      // Number of kernel basis functions
+    assert(kernels->u->n == numKernels);
+    assert(kernels->v->n == numKernels);
+    int spatialOrder = kernels->spatialOrder; // Maximum spatial polynomial order
+    assert(solution->n == numKernels * polyTerms(spatialOrder) + polyTerms(kernels->bgOrder));
+
+    // Ensure the subIndex for POIS kernels is what is expected
+    assert((kernels->type != PM_SUBTRACTION_KERNEL_POIS &&
+            kernels->type != PM_SUBTRACTION_KERNEL_SPAM &&
+            kernels->type != PM_SUBTRACTION_KERNEL_FRIES &&
+            kernels->type != PM_SUBTRACTION_KERNEL_GUNK &&
+            kernels->type != PM_SUBTRACTION_KERNEL_RINGS) ||
+           (kernels->u->data.S32[kernels->subIndex] == 0 && kernels->v->data.S32[kernels->subIndex] == 0));
+
+    int size = kernels->size;           // Kernel half-size
+    if (!kernel) {
+        kernel = psKernelAlloc(-size, size, -size, size);
+    }
+    psImageInit(kernel->image, 0.0);
+
+    for (int i = 0; i < numKernels; i++) {
+        double value = 0.0;             // Value of this component, from the sum of multiple polynomial terms
+        for (int yOrder = 0, index = i; yOrder <= spatialOrder; yOrder++) {
+            for (int xOrder = 0; xOrder <= spatialOrder - yOrder; xOrder++, index += numKernels) {
+                value += polyValues->data.F64[yOrder][xOrder] * solution->data.F64[index];
+            }
+        }
+
+        switch (kernels->type) {
+          case PM_SUBTRACTION_KERNEL_POIS: {
+              int u = kernels->u->data.S32[i]; // Offset in x
+              int v = kernels->v->data.S32[i]; // Offset in y
+              kernel->kernel[v][u] += value;
+              if (kernels->spatialOrder > 0 && i != kernels->subIndex) {
+                  // The (0,0) element is subtracted from most kernels to preserve photometric scaling
+                  kernel->kernel[0][0] -= value;
+              }
+              break;
+          }
+          /* SPAM and FRIES use the same method */
+          case PM_SUBTRACTION_KERNEL_SPAM:
+          case PM_SUBTRACTION_KERNEL_FRIES: {
+              int uStart = kernels->u->data.S32[i];
+              int uStop = kernels->uStop->data.S32[i];
+              int vStart = kernels->v->data.S32[i];
+              int vStop = kernels->vStop->data.S32[i];
+
+              // Normalising sum of kernel component to unity
+              float norm = 1.0 / (float)((uStop - uStart + 1) * (vStop - vStart + 1));
+
+              for (int v = vStart; v <= vStop; v++) {
+                  for (int u = uStart; u <= uStop; u++) {
+                      kernel->kernel[v][u] += norm * value;
+                      if (kernels->spatialOrder > 0 && i != kernels->subIndex) {
+                          // The (0,0) element is subtracted from most kernels to preserve photometric scaling
+                          kernel->kernel[0][0] -= value;
+                      }
+                  }
+              }
+              break;
+          }
+          case PM_SUBTRACTION_KERNEL_GUNK: {
+              if (i < kernels->inner) {
+                  // Using pre-calculated function
+                  psKernel *preCalc = kernels->preCalc->data[i]; // Precalculated values
+                  // Iterating over the kernel
+                  for (int v = -size; v <= size; v++) {
+                      for (int u = -size; u <= size; u++) {
+                          kernel->kernel[v][u] += preCalc->kernel[v][u] * value;
+                          // Photometric scaling is built into the preCalc kernel --- no subtraction!
+                      }
+                  }
+              } else {
+                  // Using delta function
+                  bool subtract = (kernels->spatialOrder > 0 && i != kernels->subIndex); // Subtract (0,0)?
+                  int u = kernels->u->data.S32[i]; // Offset in x
+                  int v = kernels->v->data.S32[i]; // Offset in y
+                  kernel->kernel[v][u] += value;
+                  if (subtract) {
+                      // The (0,0) element is subtracted from most kernels to preserve photometric scaling
+                      kernel->kernel[0][0] -= value;
+                  }
+              }
+              break;
+          }
+          case PM_SUBTRACTION_KERNEL_ISIS: {
+              psKernel *preCalc = kernels->preCalc->data[i]; // Precalculated values
+              // Iterating over the kernel
+              for (int v = -size; v <= size; v++) {
+                  for (int u = -size; u <= size; u++) {
+                      kernel->kernel[v][u] += preCalc->kernel[v][u] * value;
+                      // Photometric scaling is built into the preCalc kernel --- no subtraction!
+                  }
+              }
+              break;
+          }
+          case PM_SUBTRACTION_KERNEL_RINGS: {
+              if (i == kernels->subIndex) {
+                  kernel->kernel[0][0] += value;
+                  break;
+              }
+              psArray *preCalc = kernels->preCalc->data[i]; // Precalculated data
+              psVector *uCoords = preCalc->data[0]; // u coordinates
+              psVector *vCoords = preCalc->data[1]; // v coordinates
+              psVector *poly = preCalc->data[2]; // Polynomial values
+              int num = uCoords->n;     // Number of pixels
+
+              for (int j = 0; j < num; j++) {
+                  int u = uCoords->data.S32[j], v = vCoords->data.S32[j]; // Kernel coordinates
+                  kernel->kernel[v][u] += poly->data.F32[j] * value;
+              }
+              // Photometric scaling is built into the kernel --- no subtraction!
+              break;
+          }
+          default:
+            psAbort("Should never get here.");
+        }
+    }
+
+    return kernel;
+}
+
+// Subtract the (0,0) element to preserve photometric scaling
+static void convolveSub(psKernel *convolved, // Convolved image
+                        const psKernel *image, // Image being convolved
+                        int footprint  // Size of region of interest
+                        )
+{
+    // Can't use psBinaryOp because the images are of different size
+    for (int y = -footprint; y <= footprint; y++) {
+        for (int x = -footprint; x <= footprint; x++) {
+            convolved->kernel[y][x] -= image->kernel[y][x];
+        }
+    }
+    return;
+}
+
+// Generate the convolution given some offset
+static psKernel *convolveOffset(const psKernel *image, // Image to convolve (a kernel for convenience)
+                                int u, int v, // Offset to apply
+                                int footprint // Size of region of interest
+                                )
+{
+    psKernel *convolved = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Convolved image
+    int numBytes = (2 * footprint + 1) * PSELEMTYPE_SIZEOF(PS_TYPE_F32); // Number of bytes to copy
+    for (int y = -footprint; y <= footprint; y++) {
+        // Convolution with a delta function is just the value specified by the offset
+        memcpy(&convolved->kernel[y][-footprint], &image->kernel[y - v][-footprint - u], numBytes);
+    }
+    return convolved;
+}
+
+// Generate the convolved reference image
+static psKernel *convolveRef(const pmSubtractionKernels *kernels, // Kernel basis functions
+                             int index, // Kernel basis function index
+                             const psKernel *image, // Image to convolve (a kernel for convenience)
+                             int footprint // Size of region of interest
+                             )
+{
+    switch (kernels->type) {
+      case PM_SUBTRACTION_KERNEL_POIS: {
+          int u = kernels->u->data.S32[index]; // Offset in x
+          int v = kernels->v->data.S32[index]; // Offset in y
+          psKernel *convolved = convolveOffset(image, u, v, footprint); // Convolved image
+          if (kernels->spatialOrder > 0 && index != kernels->subIndex) {
+              convolveSub(convolved, image, footprint);
+          }
+          return convolved;
+      }
+        // Method for SPAM and FRIES is the same
+      case PM_SUBTRACTION_KERNEL_SPAM:
+      case PM_SUBTRACTION_KERNEL_FRIES: {
+          psKernel *convolved = psKernelAlloc(-footprint, footprint,
+                                              -footprint, footprint); // Convolved image
+          int uStart = kernels->u->data.S32[index];
+          int uStop = kernels->uStop->data.S32[index];
+          int vStart = kernels->v->data.S32[index];
+          int vStop = kernels->vStop->data.S32[index];
+          float norm = 1.0 / (uStop - uStart + 1) * (vStop - vStart + 1); // Normalisation
+          for (int y = -footprint; y <= footprint; y++) {
+              for (int x = -footprint; x <= footprint; x++) {
+                  double sum = 0.0;
+                  for (int v = vStart; v <= vStop; v++) {
+                      for (int u = uStart; u <= uStop; u++) {
+                          sum += image->kernel[y - v][x - u];
+                      }
+                  }
+                  convolved->kernel[y][x] = norm * sum;
+              }
+          }
+          if (kernels->spatialOrder > 0 && index != kernels->subIndex) {
+              convolveSub(convolved, image, footprint);
+          }
+          return convolved;
+      }
+      case PM_SUBTRACTION_KERNEL_GUNK: {
+          if (index < kernels->inner) {
+              // Photometric scaling is already built in to the precalculated kernel
+              return p_pmSubtractionConvolveStampPrecalc(image, kernels->preCalc->data[index]);
+          }
+          // Using delta function
+          int u = kernels->u->data.S32[index]; // Offset in x
+          int v = kernels->v->data.S32[index]; // Offset in y
+          psKernel *convolved = convolveOffset(image, u, v, footprint); // Convolved image
+          if (kernels->spatialOrder > 0 && index != kernels->subIndex) {
+              convolveSub(convolved, image, footprint);
+          }
+          return convolved;
+      }
+      case PM_SUBTRACTION_KERNEL_ISIS: {
+          // Photometric scaling is already built in to the precalculated kernel
+          return p_pmSubtractionConvolveStampPrecalc(image, kernels->preCalc->data[index]);
+      }
+      case PM_SUBTRACTION_KERNEL_RINGS: {
+          psKernel *convolved = psKernelAlloc(-footprint, footprint,
+                                              -footprint, footprint); // Convolved image
+          if (index == kernels->subIndex) {
+              // Simply copying over the image
+              return convolveOffset(image, 0, 0, footprint);
+          }
+
+          psArray *preCalc = kernels->preCalc->data[index]; // Precalculated data
+          psVector *uCoords = preCalc->data[0]; // u coordinates
+          psVector *vCoords = preCalc->data[1]; // v coordinates
+          psVector *poly = preCalc->data[2]; // Polynomial values
+          int num = uCoords->n;         // Number of pixels
+          for (int y = -footprint; y <= footprint; y++) {
+              for (int x = -footprint; x <= footprint; x++) {
+                  double sum = 0.0;             // Accumulated sum from convolution
+                  for (int j = 0; j < num; j++) {
+                      int u = uCoords->data.S32[j], v = vCoords->data.S32[j]; // Kernel coordinates
+                      sum += image->kernel[y - v][x - u] * poly->data.F32[j];
+                  }
+                  convolved->kernel[y][x] = sum;
+                  // Photometric scaling is built into the kernel --- no subtraction!
+              }
+          }
+          return convolved;
+      }
+      default:
+        psAbort("Should never get here.");
+    }
+    return NULL;
+}
+
+// Convolve an image using FFT
+static void convolveFFT(psImage *target,// Place the result in here
+                        const psImage *image, // Image to convolve
+                        const psImage *mask, // Mask image
+                        const psKernel *kernel, // Kernel by which to convolve
+                        psRegion region,// Region of interest
+                        float background, // Background to add
+                        int size        // Size of (square) kernel
+                        )
+{
+    psRegion border = psRegionSet(region.x0 - size, region.x1 + size,
+                                  region.y0 - size, region.y1 + size); // Add a border
+
+    // Casting away const so psImageSubset can add the child
+    psImage *subImage = psImageSubset((psImage*)image, border); // Subimage to convolve
+    psImage *subMask = mask ? psImageSubset((psImage*)mask, border) : NULL; // Subimage mask
+    psImage *convolved = psImageConvolveFFT(subImage, subMask, PM_SUBTRACTION_MASK_REF,
+                                            kernel, 0.0); // Convolution
+    psFree(subImage);
+    psFree(subMask);
+
+    // Now, we have to chop off the borders, and stick it in where it belongs
+    psImage *subConv = psImageSubset(convolved, psRegionSet(size, -size, size, -size)); // Cut off the edges
+    psImage *subTarget = psImageSubset(target, region); // Target for this subregion
+    if (background != 0.0) {
+        psBinaryOp(subTarget, subConv, "+", psScalarAlloc(background, PS_TYPE_F32));
+    } else {
+        int numBytes = subTarget->numCols * PSELEMTYPE_SIZEOF(PS_TYPE_F32); // Number of bytes to copy
+        for (int y = 0; y < subTarget->numRows; y++) {
+            memcpy(subTarget->data.F32[y], subConv->data.F32[y], numBytes);
+        }
+    }
+    psFree(subConv);
+    psFree(convolved);
+    psFree(subTarget);
+    return;
+}
+
+// Convolve an image directly
+static void convolveDirect(psImage *target, // Put the result here
+                           const psImage *image, // Image to convolve
+                           const psKernel *kernel, // Kernel by which to convolve
+                           psRegion region,// Region of interest
+                           float background, // Background to add
+                           int size        // Size of (square) kernel
+                           )
+{
+    for (int y = region.y0; y < region.y1; y++) {
+        for (int x = region.x0; x < region.x1; x++) {
+            target->data.F32[y][x] = background;
+            for (int v = -size; v <= size; v++) {
+                for (int u = -size; u <= size; u++) {
+                    target->data.F32[y][x] += kernel->kernel[v][u] *
+                        image->data.F32[y - v][x - u];
+                }
+            }
+        }
+    }
+    return;
+}
+
+// Mark a pixel as blank in the image, mask and weight
+static inline void markBlank(psImage *image, // Image to mark as blank
+                             psImage *mask, // Mask to mark as blank (or NULL)
+                             psImage *weight, // Weight map to mark as blank (or NULL)
+                             int x, int y, // Coordinates to mark blank
+                             psMaskType blank // Blank mask value
+    )
+{
+    image->data.F32[y][x] = NAN;
+    if (mask) {
+        mask->data.PS_TYPE_MASK_DATA[y][x] |= blank;
+    }
+    if (weight) {
+        weight->data.F32[y][x] = NAN;
+    }
+    return;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Semi-public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Generate the convolution given a precalculated kernel
+psKernel *p_pmSubtractionConvolveStampPrecalc(const psKernel *image, const psKernel *kernel)
+{
+    PS_ASSERT_KERNEL_NON_NULL(image, NULL);
+    PS_ASSERT_KERNEL_NON_NULL(kernel, NULL);
+
+    psImage *conv = psImageConvolveFFT(image->image, NULL, 0, kernel, 0.0); // Convolved image
+    int x0 = - image->xMin, y0 = - image->yMin; // Position of centre of convolved image
+    psKernel *convolved = psKernelAllocFromImage(conv, x0, y0); // Kernel version
+    psFree(conv);
+    return convolved;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+psImage *pmSubtractionMask(const psImage *refMask, const psImage *inMask, psMaskType maskVal,
+                           int size, int footprint, float badFrac, bool useFFT)
+{
+    PS_ASSERT_IMAGE_NON_NULL(refMask, NULL);
+    PS_ASSERT_IMAGE_TYPE(refMask, PS_TYPE_MASK, NULL);
+    if (inMask) {
+        PS_ASSERT_IMAGE_NON_NULL(inMask, NULL);
+        PS_ASSERT_IMAGE_TYPE(inMask, PS_TYPE_MASK, NULL);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(inMask, refMask, NULL);
+    }
+    PS_ASSERT_INT_NONNEGATIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(footprint, NULL);
+    if (isfinite(badFrac)) {
+        PS_ASSERT_FLOAT_LARGER_THAN(badFrac, 0.0, NULL);
+        PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(badFrac, 1.0, NULL);
+    }
+
+    // Size of the images
+    int numCols = refMask->numCols;
+    int numRows = refMask->numRows;
+
+    // Dereference inputs for convenience
+    psMaskType **inData = NULL;
+    if (inMask) {
+        inData = inMask->data.PS_TYPE_MASK_DATA;
+    }
+    psMaskType **refData = refMask->data.PS_TYPE_MASK_DATA;
+
+    // First, a pass through to determine the fraction of bad pixels
+    if (isfinite(badFrac) && badFrac != 1.0) {
+        int numBad = 0;                 // Number of bad pixels
+        for (int y = 0; y < numRows; y++) {
+            for (int x = 0; x < numCols; x++) {
+                if (inData && inData[y][x] & maskVal) {
+                    numBad++;
+                    continue;
+                }
+                if (refData[y][x] & maskVal) {
+                    numBad++;
+                }
+            }
+        }
+        if (numBad > badFrac * numCols * numRows) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                    "Fraction of bad pixels (%d/%d=%f) exceeds limit (%f)\n",
+                    numBad, numCols * numRows, (float)numBad/(float)(numCols * numRows), badFrac);
+            return NULL;
+        }
+    }
+
+    // Worried about the masks for bad pixels and bad stamps colliding, so make our own mask
+    psImage *mask = psImageAlloc(numCols, numRows, PS_TYPE_MASK); // The global mask
+    psImageInit(mask, 0);
+    psMaskType **maskData = mask->data.PS_TYPE_MASK_DATA; // Dereference for convenience
+
+    // Block out a border around the edge of the image
+
+    // Bottom stripe
+    for (int y = 0; y < PS_MIN(size + footprint, numRows); y++) {
+        for (int x = 0; x < numCols; x++) {
+            maskData[y][x] |= PM_SUBTRACTION_MASK_BORDER;
+        }
+    }
+    // Either side
+    for (int y = PS_MIN(size + footprint, numRows); y < numRows - size - footprint; y++) {
+        for (int x = 0; x < PS_MIN(size + footprint, numCols); x++) {
+            maskData[y][x] |= PM_SUBTRACTION_MASK_BORDER;
+        }
+        for (int x = PS_MAX(numCols - size - footprint, 0); x < numCols; x++) {
+            maskData[y][x] |= PM_SUBTRACTION_MASK_BORDER;
+        }
+    }
+    // Top stripe
+    for (int y = PS_MAX(numRows - size - footprint, 0); y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            maskData[y][x] |= PM_SUBTRACTION_MASK_BORDER;
+        }
+    }
+
+    // XXX Could do something smarter here --- we will get images that are predominantly masked (where the
+    // skycell isn't overlapped by a large fraction by the observation), so that convolving around every bad
+    // pixel is wasting time.  As a first cut, I've put in a check on the fraction of bad pixels, but we could
+    // imagine looking for the edge of big regions and convolving just at the edge.  As a second cut, allow
+    // use of FFT convolution.
+
+    for (int y = 0; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            if (inData && inData[y][x] & maskVal) {
+                maskData[y][x] |= PM_SUBTRACTION_MASK_INPUT;
+            }
+            if (refData[y][x] & maskVal) {
+                maskData[y][x] |= PM_SUBTRACTION_MASK_REF;
+            }
+        }
+    }
+
+    // Block out the entire stamp footprint around bad input pixels.
+
+    // We want to block out with the CONVOLVE mask anything that would be bad if we convolved with a bad
+    // reference pixel (within 'size').  Then we want to block out with the FOOTPRINT mask everything within a
+    // footprint's distance of those (within 'footprint').
+
+    if (useFFT) {
+        if (!psImageConvolveMaskFFT(mask, mask, PM_SUBTRACTION_MASK_INPUT, PM_SUBTRACTION_MASK_FOOTPRINT,
+                                    -footprint, footprint, -footprint, footprint, 0.5)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to convolve bad input pixels.");
+            psFree(mask);
+            return NULL;
+        }
+        if (!psImageConvolveMaskFFT(mask, mask, PM_SUBTRACTION_MASK_REF, PM_SUBTRACTION_MASK_CONVOLVE,
+                                    -size, size, -size, size, 0.5)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to convolve bad reference pixels.");
+            psFree(mask);
+            return NULL;
+        }
+        if (!psImageConvolveMaskFFT(mask, mask, PM_SUBTRACTION_MASK_CONVOLVE, PM_SUBTRACTION_MASK_FOOTPRINT,
+                                    -footprint, footprint, -footprint, footprint, 0.5)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to reconvolve bad reference pixels.");
+            psFree(mask);
+            return NULL;
+        }
+    } else {
+        if (!psImageConvolveMaskDirect(mask, mask, PM_SUBTRACTION_MASK_INPUT, PM_SUBTRACTION_MASK_FOOTPRINT,
+                                       -footprint, footprint, -footprint, footprint)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to convolve bad input pixels.");
+            psFree(mask);
+            return NULL;
+        }
+        if (!psImageConvolveMaskDirect(mask, mask, PM_SUBTRACTION_MASK_REF, PM_SUBTRACTION_MASK_CONVOLVE,
+                                   -size, size, -size, size)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to convolve bad reference pixels.");
+            psFree(mask);
+            return NULL;
+        }
+        if (!psImageConvolveMaskDirect(mask, mask, PM_SUBTRACTION_MASK_CONVOLVE,
+                                       PM_SUBTRACTION_MASK_FOOTPRINT,
+                                       -footprint, footprint, -footprint, footprint)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to reconvolve bad reference pixels.");
+            psFree(mask);
+            return NULL;
+        }
+    }
+
+    return mask;
+}
+
+bool pmSubtractionConvolveStamp(pmSubtractionStamp *stamp, const pmSubtractionKernels *kernels, int footprint)
+{
+    PS_ASSERT_PTR_NON_NULL(stamp, false);
+    PS_ASSERT_KERNEL_NON_NULL(stamp->reference, false);
+    PS_ASSERT_PTR_NON_NULL(kernels, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(kernels->u, kernels->v, false);
+    PS_ASSERT_INT_NONNEGATIVE(footprint, false);
+
+    if (stamp->status != PM_SUBTRACTION_STAMP_CALCULATE) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Stamp not marked for calculation.");
+        return false;
+    }
+
+    if (stamp->convolutions) {
+        // Already done
+        return true;
+    }
+
+    stamp->convolutions = psArrayAlloc(kernels->num);
+    psKernel *reference = stamp->reference; // Reference postage stamp
+
+    for (int i = 0; i < kernels->num; i++) {
+        stamp->convolutions->data[i] = convolveRef(kernels, i, reference, footprint);
+    }
+
+    return true;
+}
+
+
+bool pmSubtractionCalculateEquation(pmSubtractionStampList *stamps, const pmSubtractionKernels *kernels,
+                                    int footprint)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PS_ASSERT_PTR_NON_NULL(kernels, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(kernels->u, kernels->v, false);
+    PS_ASSERT_INT_NONNEGATIVE(footprint, false);
+
+    int spatialOrder = kernels->spatialOrder; // Maximum order of spatial variation
+    int numKernels = kernels->num;      // Number of kernel basis functions
+    int numSpatial = polyTerms(spatialOrder); // Number of spatial variations
+    int numBackground = polyTerms(kernels->bgOrder); // Number of background terms
+
+    // Total number of parameters to solve for: coefficient of each kernel basis function, multipled by the
+    // number of coefficients for the spatial polynomial, and a constant background offset.
+    int numParams = numKernels * numSpatial + numBackground;
+    int bgIndex = numParams - numBackground; // Index in matrix for the background
+
+    // We iterate over each stamp, allocate the matrix and vectors if
+    // necessary, and then calculate those matrix/vectors.
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (stamp->status != PM_SUBTRACTION_STAMP_CALCULATE) {
+            continue;
+        }
+
+        psImage *stampMatrix = stamp->matrix; // Least-squares matrix for this stamp
+        psVector *stampVector = stamp->vector; // Least-squares vector for this stamp
+
+        if (!stampMatrix) {
+            stamp->matrix = stampMatrix = psImageAlloc(numParams, numParams, PS_TYPE_F64);
+        }
+        assert(stampMatrix->type.type == PS_TYPE_F64);
+        assert(stampMatrix->numCols == numParams && stampMatrix->numRows == numParams);
+        psImageInit(stampMatrix, 0.0);
+
+        if (!stampVector) {
+            stamp->vector = stampVector = psVectorAlloc(numParams, PS_TYPE_F64);
+        }
+        assert(stampVector->type.type == PS_TYPE_F64);
+        assert(stampVector->n == numParams);
+        psVectorInit(stampVector, 0.0);
+
+        psKernel *input = stamp->input; // Input postage stamp
+        psKernel *weight = stamp->weight; // Weight map postage stamp
+
+        // Generate convolutions of the reference
+        if (!pmSubtractionConvolveStamp(stamp, kernels, footprint)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to convolve stamp %d.", i);
+            return NULL;
+        }
+
+        psArray *convolutions = stamp->convolutions; // Convolutions of the reference for each kernel
+        psImage *polyValues = spatialPolyValues(spatialOrder, stamp->xNorm, stamp->yNorm); // Polynomial terms
+
+        // Generate least-squares vector and matrix
+        bool bad = false;           // Are there bad values?
+        for (int j = 0; j < numKernels; j++) {
+            psKernel *jConv = convolutions->data[j]; // Convolution for j-th element
+
+            // Generate matrix
+            for (int k = j; k < numKernels; k++) {
+                psKernel *kConv = convolutions->data[k]; // Convolution for k-th element
+                double sumCC = 0.0; // Sum of the convolution products
+                for (int y = - footprint; y <= footprint; y++) {
+                    for (int x = - footprint; x <= footprint; x++) {
+                        sumCC += jConv->kernel[y][x] * kConv->kernel[y][x] / weight->kernel[y][x];
+                    }
+                }
+                if (!isfinite(sumCC)) {
+                    bad = true;
+                }
+                // Generate the pseudo-convolutions from the spatial polynomial terms
+                for (int jyOrder = 0, jIndex = j; jyOrder <= spatialOrder; jyOrder++) {
+                    for (int jxOrder = 0; jxOrder <= spatialOrder - jyOrder;
+                         jxOrder++, jIndex += numKernels) {
+                        for (int kyOrder = 0, kIndex = k; kyOrder <= spatialOrder; kyOrder++) {
+                            for (int kxOrder = 0; kxOrder <= spatialOrder - kyOrder;
+                                 kxOrder++, kIndex += numKernels) {
+                                double convPoly = sumCC * polyValues->data.F64[jyOrder][jxOrder] *
+                                    polyValues->data.F64[kyOrder][kxOrder]; // Temporary value
+                                stampMatrix->data.F64[jIndex][kIndex] = convPoly;
+                                stampMatrix->data.F64[kIndex][jIndex] = convPoly;
+                            }
+                        }
+                    }
+                }
+            }
+
+            // Vector and background term for matrix
+            double sumC = 0.0;      // Sum of the convolution
+            double sumIC = 0.0;     // Sum of the convolution/input product
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    double convProduct = jConv->kernel[y][x] / weight->kernel[y][x]; // Convolution / noise^2
+                    sumC += convProduct;
+                    sumIC += input->kernel[y][x] * convProduct;
+                }
+            }
+            if (!isfinite(sumC) || !isfinite(sumIC)) {
+                bad = true;
+            }
+            for (int jyOrder = 0, jIndex = j; jyOrder <= spatialOrder; jyOrder++) {
+                for (int jxOrder = 0; jxOrder <= spatialOrder - jyOrder;
+                     jxOrder++, jIndex += numKernels) {
+                    stampVector->data.F64[jIndex] = sumIC * polyValues->data.F64[jyOrder][jxOrder];
+
+                    double convPoly = sumC * polyValues->data.F64[jyOrder][jxOrder]; // Value
+                    stampMatrix->data.F64[jIndex][bgIndex] = convPoly;
+                    stampMatrix->data.F64[bgIndex][jIndex] = convPoly;
+                }
+            }
+
+        }
+        psFree(polyValues);
+
+        // Background only terms
+        double sum1 = 0.0;              // Sum of the weighting
+        double sumI = 0.0;              // Sum of the input
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                double invNoise2 = 1.0 / weight->kernel[y][x]; // Inverse noise, squared
+                sum1 += invNoise2;
+                sumI += input->kernel[y][x] * invNoise2;
+            }
+        }
+        if (!isfinite(sum1) || !isfinite(sumI)) {
+            bad = true;
+        }
+        stampMatrix->data.F64[bgIndex][bgIndex] = sum1;
+        stampVector->data.F64[bgIndex] = sumI;
+
+        if (bad) {
+            stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
+            psTrace("psModules.imcombine", 3, "Rejecting stamp %d (%d,%d) because of bad equation\n",
+                    i, (int)(stamp->x + 0.5), (int)(stamp->y + 0.5));
+        } else {
+            stamp->status = PM_SUBTRACTION_STAMP_USED;
+        }
+
+        if (psTraceGetLevel("psModules.imcombine.equation") >= 10) {
+            psString matrixName = NULL;
+            psStringAppend(&matrixName, "matrix%d.fits", i);
+            psFits *matrixFile = psFitsOpen(matrixName, "w");
+            psFree(matrixName);
+            psFitsWriteImage(matrixFile, NULL, stampMatrix, 0, NULL);
+            psFitsClose(matrixFile);
+        }
+
+    }
+
+    return true;
+}
+
+
+psVector *pmSubtractionSolveEquation(psVector *solution, const pmSubtractionStampList *stamps)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, NULL);
+
+    // Check inputs, while summing the stamp matrices and vectors
+    long numParams = -1;                // Number of parameters
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        PS_ASSERT_PTR_NON_NULL(stamp, NULL);
+        if (stamp->status != PM_SUBTRACTION_STAMP_USED) {
+            continue;
+        }
+        if (numParams == -1) {
+            numParams = stamp->vector->n;
+        } else {
+            PS_ASSERT_VECTOR_SIZE(stamp->vector, numParams, NULL);
+        }
+        PS_ASSERT_VECTOR_TYPE(stamp->vector, PS_TYPE_F64, NULL);
+        PS_ASSERT_IMAGE_TYPE(stamp->matrix, PS_TYPE_F64, NULL);
+        PS_ASSERT_IMAGE_SIZE(stamp->matrix, numParams, numParams, NULL);
+    }
+    if (numParams == -1) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "No suitable stamps found.");
+        return NULL;
+    }
+
+    if (solution) {
+        PS_ASSERT_VECTOR_TYPE(solution, PS_TYPE_F64, NULL);
+        PS_ASSERT_VECTOR_SIZE(solution, numParams, NULL);
+    }
+
+    // Accumulate the least-squares matricies and vectors
+    psImage *sumMatrix = psImageAlloc(numParams, numParams, PS_TYPE_F64); // Combined matrix for all stamps
+    psVector *sumVector = psVectorAlloc(numParams, PS_TYPE_F64); // Combined vector for all stamps
+    psVectorInit(sumVector, 0.0);
+    psImageInit(sumMatrix, 0.0);
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (stamp->status == PM_SUBTRACTION_STAMP_USED) {
+            (void)psBinaryOp(sumMatrix, sumMatrix, "+", stamp->matrix);
+            (void)psBinaryOp(sumVector, sumVector, "+", stamp->vector);
+        }
+    }
+
+    psVector *permutation = NULL;       // Permutation vector, required for LU decomposition
+    psImage *luMatrix = psMatrixLUD(NULL, &permutation, sumMatrix);
+    psFree(sumMatrix);
+    if (!luMatrix) {
+        psError(PS_ERR_UNKNOWN, true, "LU Decomposition of least-squares matrix failed.\n");
+        psFree(sumVector);
+        psFree(luMatrix);
+        psFree(permutation);
+        return NULL;
+    }
+
+    solution = psMatrixLUSolve(solution, luMatrix, sumVector, permutation);
+    psFree(sumVector);
+    psFree(luMatrix);
+    psFree(permutation);
+    if (!solution) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to solve the least-squares system.\n");
+        return NULL;
+    }
+
+    if (psTraceGetLevel("psModules.imcombine") >= 7) {
+        for (int i = 0; i < solution->n; i++) {
+            printf("       Solution %d: %f\n", i, solution->data.F64[i]);
+        }
+    }
+
+    return solution;
+}
+
+
+int pmSubtractionRejectStamps(pmSubtractionStampList *stamps, psImage *subMask, const psVector *solution,
+                              int footprint, float sigmaRej, const pmSubtractionKernels *kernels)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, -1);
+    PS_ASSERT_IMAGE_NON_EMPTY(subMask, -1);
+    PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_MASK, -1);
+    PS_ASSERT_VECTOR_NON_NULL(solution, -1);
+    PS_ASSERT_VECTOR_TYPE(solution, PS_TYPE_F64, -1);
+    PS_ASSERT_PTR_NON_NULL(kernels, -1);
+    PS_ASSERT_VECTOR_SIZE(solution, kernels->num * polyTerms(kernels->spatialOrder) +
+                          polyTerms(kernels->bgOrder), -1);
+    PS_ASSERT_VECTOR_SIZE(kernels->u, kernels->num, -1);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(kernels->u, kernels->v, -1);
+
+    // Measure deviations
+    psVector *deviations = psVectorAlloc(stamps->num, PS_TYPE_F32); // Mean deviation for stamps
+    double totalSquareDev = 0.0;        // Total square deviation from zero
+    int numStamps = 0;                  // Number of used stamps
+    int numKernels = kernels->num;      // Number of kernels
+    int spatialOrder = kernels->spatialOrder; // Order of kernel spatial variations
+    double devNorm = 1.0 / PS_SQR(2 * footprint + 1); // Normalisation for deviations
+    {
+        float background = solution->data.F64[solution->n-1]; // The difference in background
+
+        for (int i = 0; i < stamps->num; i++) {
+            pmSubtractionStamp *stamp = stamps->stamps->data[i]; // The stamp of interest
+            if (stamp->status != PM_SUBTRACTION_STAMP_USED) {
+                continue;
+            }
+
+            psImage *polyValues = spatialPolyValues(kernels->spatialOrder, stamp->xNorm,
+                                                    stamp->yNorm); // Polynomial terms
+
+            psKernel *input = stamp->input; // Input postage stamp
+            psKernel *weight = stamp->weight; // Weight postage stamp
+            psArray *convolutions = stamp->convolutions; // Convolutions of reference image for each kernel
+#ifdef TESTING
+            psKernel *residual = psKernelAlloc(-footprint, footprint, -footprint, footprint);
+#endif
+            float deviation = 0.0;      // Deviation for this stamp
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    float conv = background; // The value of the convolution
+                    for (int j = 0; j < numKernels; j++) {
+                        psKernel *convolution = convolutions->data[j]; // Convolution of reference
+                        double polynomial = 0.0; // Value of the polynomial
+                        for (int yOrder = 0, index = j; yOrder <= spatialOrder; yOrder++) {
+                            for (int xOrder = 0; xOrder <= spatialOrder - yOrder;
+                                 xOrder++, index += numKernels) {
+                                polynomial += solution->data.F64[index] *
+                                    polyValues->data.F64[yOrder][xOrder];
+                            }
+                        }
+                        conv += convolution->kernel[y][x] * polynomial;
+                    }
+                    float diff = input->kernel[y][x] - conv;
+                    deviation += PS_SQR(diff) / weight->kernel[y][x];
+#ifdef TESTING
+                    residual->kernel[y][x] = diff;
+#endif
+                }
+            }
+            psFree(polyValues);
+
+            deviations->data.F32[i] = devNorm * deviation;
+            psTrace("psModules.imcombine", 5, "Deviation for stamp %d (%d,%d): %f\n",
+                    i, (int)stamp->x, (int)stamp->y, deviations->data.F32[i]);
+            totalSquareDev += PS_SQR(deviations->data.F32[i]);
+            numStamps++;
+
+#ifdef TESTING
+            {
+                psString filename = NULL;
+                psStringAppend(&filename, "resid_%03d.fits", i);
+                psFits *fits = psFitsOpen(filename, "w");
+                psFree(filename);
+                psFitsWriteImage(fits, NULL, residual->image, 0, NULL);
+                psFitsClose(fits);
+            }
+            psFree(residual);
+#endif
+
+        }
+    }
+
+    float limit = sigmaRej * sqrt(totalSquareDev / numStamps); // Limit on maximum deviation
+    psTrace("psModules.imcombine", 1, "Deviation limit: %f\n", limit);
+
+    int numRejected = 0;
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (stamp->status == PM_SUBTRACTION_STAMP_USED && deviations->data.F32[i] > limit) {
+            // Mask out the stamp in the image so you it's not found again
+            psTrace("psModules.imcombine", 3, "Rejecting stamp %d (%d,%d)\n", i,
+                    (int)(stamp->x + 0.5), (int)(stamp->y + 0.5));
+            numRejected++;
+            for (int y = stamp->y - footprint; y <= stamp->y + footprint; y++) {
+                for (int x = stamp->x - footprint; x <= stamp->x + footprint; x++) {
+                    subMask->data.PS_TYPE_MASK_DATA[y][x] |= PM_SUBTRACTION_MASK_REJ;
+                }
+            }
+
+            // Set stamp for replacement
+            stamp->x = 0;
+            stamp->y = 0;
+            stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
+            // Recalculate convolutions
+            psFree(stamp->convolutions);
+            stamp->convolutions = NULL;
+        }
+    }
+
+    psFree(deviations);
+
+    return numRejected;
+}
+
+psImage *pmSubtractionKernelImage(const psVector *solution, const pmSubtractionKernels *kernels,
+                                  float x, float y
+                                 )
+{
+    PS_ASSERT_VECTOR_NON_NULL(solution, NULL);
+    PS_ASSERT_PTR_NON_NULL(kernels, NULL);
+    PS_ASSERT_VECTOR_SIZE(solution, kernels->num * polyTerms(kernels->spatialOrder) +
+                          polyTerms(kernels->bgOrder), NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NULL);
+    PS_ASSERT_VECTOR_SIZE(kernels->u, kernels->num, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(kernels->u, kernels->v, NULL);
+
+    // Precalulate polynomial values
+    psImage *polyValues = spatialPolyValues(kernels->spatialOrder, x, y);
+
+    // The appropriate kernel
+    psKernel *kernel = solvedKernel(NULL, solution, kernels, polyValues);
+
+    psFree(polyValues);
+
+    psImage *image = psMemIncrRefCounter(kernel->image); // Image of the kernel
+    psFree(kernel);
+
+    return image;
+}
+
+psArray *pmSubtractionKernelSolutions(const psVector *solution, const pmSubtractionKernels *kernels,
+                                      float x, float y
+                                      )
+{
+    PS_ASSERT_VECTOR_NON_NULL(solution, NULL);
+    PS_ASSERT_PTR_NON_NULL(kernels, NULL);
+    PS_ASSERT_VECTOR_SIZE(solution, kernels->num * polyTerms(kernels->spatialOrder) +
+                          polyTerms(kernels->bgOrder), NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NULL);
+    PS_ASSERT_VECTOR_SIZE(kernels->u, kernels->num, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(kernels->u, kernels->v, NULL);
+
+    psArray *images = psArrayAlloc(solution->n - 1); // Images of each kernel to return
+    psVector *fakeSolution = psVectorAlloc(solution->n, PS_TYPE_F64); // Fake solution vector
+    psVectorInit(fakeSolution, 0.0);
+
+    for (int i = 0; i < solution->n - 1; i++) {
+        fakeSolution->data.F64[i] = solution->data.F64[i];
+        images->data[i] = pmSubtractionKernelImage(fakeSolution, kernels, x, y);
+        fakeSolution->data.F64[i] = 0.0;
+    }
+
+    psFree(fakeSolution);
+
+    return images;
+}
+
+bool pmSubtractionConvolve(psImage **outImage, psImage **outWeight, psImage **outMask,
+                           const psImage *inImage, const psImage *inWeight, const psImage *subMask,
+                           psMaskType blank, const psRegion *region, const psVector *solution,
+                           const pmSubtractionKernels *kernels, bool useFFT)
+{
+    PS_ASSERT_IMAGE_NON_NULL(inImage, false);
+    PS_ASSERT_IMAGE_TYPE(inImage, PS_TYPE_F32, false);
+    if (subMask) {
+        PS_ASSERT_IMAGE_NON_NULL(subMask, false);
+        PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_MASK, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(subMask, inImage, false);
+    }
+    if (inWeight) {
+        PS_ASSERT_IMAGE_NON_NULL(inWeight, false);
+        PS_ASSERT_IMAGE_TYPE(inWeight, PS_TYPE_F32, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(inWeight, inImage, false);
+    }
+    PS_ASSERT_PTR_NON_NULL(outImage, false);
+    if (*outImage) {
+        PS_ASSERT_IMAGE_NON_NULL(*outImage, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(*outImage, inImage, false);
+        PS_ASSERT_IMAGE_TYPE(*outImage, PS_TYPE_F32, false);
+    }
+    if (outMask && *outMask) {
+        PS_ASSERT_IMAGE_NON_NULL(*outMask, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(*outMask, inImage, false);
+        PS_ASSERT_IMAGE_TYPE(*outMask, PS_TYPE_MASK, false);
+        PS_ASSERT_IMAGE_NON_NULL(subMask, false);
+    }
+    if (outWeight && *outWeight) {
+        PS_ASSERT_IMAGE_NON_NULL(*outWeight, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(*outWeight, inImage, false);
+        PS_ASSERT_IMAGE_TYPE(*outWeight, PS_TYPE_F32, false);
+        PS_ASSERT_IMAGE_NON_NULL(inWeight, false);
+    }
+    PS_ASSERT_VECTOR_NON_NULL(solution, false);
+    PS_ASSERT_VECTOR_TYPE(solution, PS_TYPE_F64, false);
+    PS_ASSERT_PTR_NON_NULL(kernels, false);
+    PS_ASSERT_VECTOR_SIZE(solution, kernels->num * polyTerms(kernels->spatialOrder) +
+                          polyTerms(kernels->bgOrder), -1);
+    PS_ASSERT_VECTOR_SIZE(kernels->u, kernels->num, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(kernels->u, kernels->v, false);
+    if (region) {
+        if (psRegionIsNaN(*region)) {
+            psString string = psRegionToString(*region);
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Input region (%s) contains NAN values", string);
+            psFree(string);
+            return false;
+        }
+        if (region->x0 < 0 || region->x1 > inImage->numCols ||
+            region->y0 < 0 || region->y1 > inImage->numRows) {
+            psString string = psRegionToString(*region);
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Input region (%s) does not fit in image (%dx%d)",
+                    string, inImage->numCols, inImage->numRows);
+            psFree(string);
+            return false;
+        }
+    }
+
+    int numBackground = polyTerms(kernels->bgOrder); // Number of background terms
+    float background = solution->data.F64[solution->n - numBackground]; // The difference in background
+    int numCols = inImage->numCols, numRows = inImage->numRows; // Image dimensions
+
+    psImage *convImage = *outImage;    // Convolved image
+    if (!convImage) {
+        *outImage = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        convImage = *outImage;
+    }
+    psImage *convMask = NULL;           // Convolved mask image
+    if (outMask && subMask) {
+        if (!*outMask) {
+            *outMask = psImageAlloc(numCols, numRows, PS_TYPE_MASK);
+        }
+        convMask = *outMask;
+        psImageInit(convMask, 0);
+    }
+    psImage *convWeight = NULL;         // Convolved weight (variance) image
+    if (outWeight && inWeight) {
+        if (!*outWeight) {
+            *outWeight = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        }
+        convWeight = *outWeight;
+        psImageInit(convWeight, 0.0);
+    }
+
+    int size = kernels->size;           // Half-size of kernel
+    int fullSize = 2 * size + 1;        // Full size of kernel
+    psImage *polyValues = NULL;         // Pre-calculated polynomial values
+
+    psKernel *kernelImage = NULL;       // Kernel for the image
+    psKernel *kernelWeight = NULL;      // Kernel for the weight map
+
+    // Get region for convolution: [xMin:xMax,yMin:yMax]
+    int xMin = size, xMax = numCols - size;
+    int yMin = size, yMax = numRows - size;
+    if (region) {
+        xMin = PS_MAX(region->x0, xMin);
+        xMax = PS_MIN(region->x1, xMax);
+        yMin = PS_MAX(region->y0, yMin);
+        yMax = PS_MIN(region->y1, yMax);
+    }
+
+    for (int j = yMin; j < yMax; j += fullSize) {
+        int ySubMax = PS_MIN(j + fullSize, yMax); // Range for subregion of interest
+        for (int i = xMin; i < xMax; i += fullSize) {
+            int xSubMax = PS_MIN(i + fullSize, xMax); // Range for subregion of interest
+
+            // Only generate polynomial values every kernel footprint, since we have already assumed
+            // (with the stamps) that it does not vary rapidly on this scale.
+            psFree(polyValues);
+            polyValues = spatialPolyValues(kernels->spatialOrder,
+                                           2.0 * (float)(i + size + 1 - numCols/2.0) / (float)numCols,
+                                           2.0 * (float)(j + size + 1 - numRows/2.0) / (float)numRows);
+
+            kernelImage = solvedKernel(kernelImage, solution, kernels, polyValues);
+            if (inWeight) {
+                kernelWeight = varianceKernel(kernelImage);
+            }
+
+
+            if (useFFT) {
+                // Use Fast Fourier Transform to do the convolution
+                // This provides a big speed-up for large kernels
+                convolveFFT(convImage, inImage, subMask, kernelImage, psRegionSet(i, xSubMax, j, ySubMax),
+                            background, size);
+                if (inWeight) {
+                    convolveFFT(convWeight, inWeight, subMask, kernelWeight,
+                                psRegionSet(i, xSubMax, j, ySubMax), 0.0, size);
+                }
+            } else {
+                convolveDirect(convImage, inImage, kernelImage, psRegionSet(i, xSubMax, j, ySubMax),
+                               background, size);
+                if (inWeight) {
+                    convolveDirect(convWeight, inWeight, kernelWeight, psRegionSet(i, xSubMax, j, ySubMax),
+                                   0.0, size);
+                }
+            }
+
+            // Propagate the mask
+            for (int y = j; y < ySubMax; y++) {
+                for (int x = i; x < xSubMax; x++) {
+                    if (subMask && (subMask->data.PS_TYPE_MASK_DATA[y][x] &
+                                    (PM_SUBTRACTION_MASK_INPUT | PM_SUBTRACTION_MASK_CONVOLVE))) {
+                        convMask->data.PS_TYPE_MASK_DATA[y][x] |= blank;
+                        convImage->data.F32[y][x] = NAN;
+                        if (inWeight) {
+                            convWeight->data.F32[y][x] = NAN;
+                        }
+                    }
+                }
+            }
+
+        }
+    }
+    psFree(kernelImage);
+    psFree(kernelWeight);
+    psFree(polyValues);
+
+    return true;
+}
+
+bool pmSubtractionBorder(psImage *image, psImage *weight, psImage *mask,
+                         int size, psMaskType blank)
+{
+    PS_ASSERT_IMAGE_NON_NULL(image, false);
+    PS_ASSERT_IMAGE_TYPE(image, PS_TYPE_F32, false);
+    if (mask) {
+        PS_ASSERT_IMAGE_NON_NULL(mask, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(mask, image, false);
+        PS_ASSERT_IMAGE_TYPE(mask, PS_TYPE_MASK, false);
+    }
+    if (weight) {
+        PS_ASSERT_IMAGE_NON_NULL(weight, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(weight, image, false);
+        PS_ASSERT_IMAGE_TYPE(weight, PS_TYPE_F32, false);
+    }
+
+    int numCols = image->numCols, numRows = image->numRows; // Image dimensions
+
+    for (int y = size; y < numRows - size; y++) {
+        for (int x = 0; x < size; x++) {
+            markBlank(image, mask, weight, x, y, blank);
+        }
+        for (int x = numCols - size; x < numCols; x++) {
+            markBlank(image, mask, weight, x, y, blank);
+        }
+    }
+    for (int y = 0; y < size; y++) {
+        for (int x = 0; x < numCols; x++) {
+            markBlank(image, mask, weight, x, y, blank);
+        }
+    }
+    for (int y = numRows - size; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            markBlank(image, mask, weight, x, y, blank);
+        }
+    }
+
+    return true;
+}
+
+
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtraction.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtraction.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtraction.h	(revision 15358)
@@ -0,0 +1,112 @@
+/* @file pmSubtraction.h
+ *
+ * PSF-matched image subtraction, based on the Alard & Lupton (1998) and Alard (2000) methods.
+ *
+ * @author Paul Price, IfA
+ * @author GLG, MHPCC
+ *
+ * @version $Revision: 1.19 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-10-17 02:45:40 $
+ *
+ * Copyright 2004-207 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_SUBTRACTION_H
+#define PM_SUBTRACTION_H
+
+#include <pslib.h>
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionStamps.h"
+
+/// @addtogroup imcombine Image Combinations
+/// @{
+
+typedef enum {
+    PM_SUBTRACTION_MASK_CLEAR     = 0x00, // No masking
+    PM_SUBTRACTION_MASK_REF       = 0x01, // Reference image is bad
+    PM_SUBTRACTION_MASK_INPUT     = 0x02, // Input image is bad
+    PM_SUBTRACTION_MASK_CONVOLVE  = 0x04, // If convolved, would be bad
+    PM_SUBTRACTION_MASK_FOOTPRINT = 0x08, // Bad pixel within the stamp footprint
+    PM_SUBTRACTION_MASK_BORDER    = 0x10, // Image border
+    PM_SUBTRACTION_MASK_REJ       = 0x20, // Previously tried as a stamp, and rejected
+} pmSubtractionMasks;
+
+/// Generate a mask for use in the subtraction process
+psImage *pmSubtractionMask(const psImage *refMask, ///< Mask for the reference image (will be convolved)
+                           const psImage *inMask, ///< Mask for the input image, or NULL
+                           psMaskType maskVal, ///< Value to mask out
+                           int size, ///< Half-size of the kernel (pmSubtractionKernels.size)
+                           int footprint, ///< Half-size of the kernel footprint
+                           float badFrac, ///< Maximum fraction of bad input pixels to accept
+                           bool useFFT  ///< Use FFT to do convolution?
+    );
+
+/// Convolve the reference stamp with the kernel components
+bool pmSubtractionConvolveStamp(pmSubtractionStamp *stamp, ///< Stamp to convolve
+                                const pmSubtractionKernels *kernels, ///< Kernel parameters
+                                int footprint ///< Half-size of region over which to calculate equation
+    );
+
+/// Calculate the least-squares equation to match the image quality
+bool pmSubtractionCalculateEquation(pmSubtractionStampList *stamps, ///< Stamps
+                                    const pmSubtractionKernels *kernels, ///< Kernel parameters
+                                    int footprint ///< Half-size of region over which to calculate equation
+                                    );
+
+/// Solve the least-squares equation to match the image quality
+psVector *pmSubtractionSolveEquation(psVector *solution, ///< Solution vector, or NULL
+                                     const pmSubtractionStampList *stamps ///< Stamps
+                                     );
+
+/// Reject stamps
+int pmSubtractionRejectStamps(pmSubtractionStampList *stamps, ///< Stamps
+                              psImage *subMask, ///< Subtraction mask
+                              const psVector *solution, ///< Solution vector
+                              int footprint, ///< Region to mask if stamp is bad
+                              float sigmaRej, ///< Number of RMS deviations above zero at which to reject
+                              const pmSubtractionKernels *kernels ///< Kernel parameters
+    );
+
+/// Generate an image of the convolution kernel
+psImage *pmSubtractionKernelImage(const psVector *solution, ///< Solution vector
+                                  const pmSubtractionKernels *kernels, ///< Kernel parameters
+                                  float x, float y ///< Normalised position [-1,1] for which to generate image
+                                  );
+
+/// Generate images of the convolution kernel elements
+psArray *pmSubtractionKernelSolutions(const psVector *solution, ///< Solution vector
+                                      const pmSubtractionKernels *kernels, ///< Kernel parameters
+                                      float x, float y ///< Normalised position [-1,1] for images
+    );
+
+/// Convolve image in preparation for subtraction
+bool pmSubtractionConvolve(psImage **outImage, ///< Output image
+                           psImage **outWeight, ///< Output weight map (or NULL)
+                           psImage **outMask, ///< Output mask (or NULL)
+                           const psImage *inImage, ///< Input image
+                           const psImage *inWeight, ///< Input weight map (or NULL)
+                           const psImage *subMask, ///< Subtraction mask (or NULL)
+                           psMaskType blank, ///< Mask value for blank regions
+                           const psRegion *region, ///< Region to convolve (or NULL)
+                           const psVector *solution, ///< The solution vector
+                           const pmSubtractionKernels *kernels, ///< Kernel parameters
+                           bool useFFT  ///< Use Fast Fourier Transform for the convolution?
+    );
+
+/// Mark the non-convolved part of the image as blank
+bool pmSubtractionBorder(psImage *image,///< Image
+                         psImage *weight, ///< Weight map (or NULL)
+                         psImage *mask, ///< Mask (or NULL)
+                         int size,      ///< Kernel half-size
+                         psMaskType blank ///< Mask value for blank regions
+    );
+
+/// Generate the convolution of an image, given a precalculated kernel
+///
+/// The 'image' is a kernel for convenience --- intended to be a stamp
+psKernel *p_pmSubtractionConvolveStampPrecalc(const psKernel *image, ///< Image to convolve
+                                              const psKernel *kernel ///< Kernel by which to convolve
+    );
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionKernels.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionKernels.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionKernels.c	(revision 15358)
@@ -0,0 +1,681 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmSubtractionKernels.h"
+
+#define RINGS_BUFFER 10                 // Buffer size for RINGS data
+
+
+// Free function for pmSubtractionKernels
+static void subtractionKernelsFree(pmSubtractionKernels *kernels)
+{
+    psFree(kernels->description);
+    psFree(kernels->u);
+    psFree(kernels->v);
+    psFree(kernels->widths);
+    psFree(kernels->uStop);
+    psFree(kernels->vStop);
+    psFree(kernels->preCalc);
+}
+
+// Raise a number to an integer power
+static inline long power(int value,     // Value
+                         int exp        // Exponent
+    )
+{
+    if (exp == 0) {
+        return 1.0;
+    }
+    long result = value;               // Result to return
+    for (int i = 2; i <= exp; i++) {
+        result *= value;
+    }
+    return result;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Semi-public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool p_pmSubtractionKernelsAddGrid(pmSubtractionKernels *kernels, int start, int size)
+{
+    PS_ASSERT_PTR_NON_NULL(kernels, false);
+    PS_ASSERT_VECTOR_NON_NULL(kernels->u, false);
+    PS_ASSERT_VECTOR_NON_NULL(kernels->v, false);
+    PS_ASSERT_VECTOR_NON_NULL(kernels->widths, false);
+    PS_ASSERT_VECTOR_TYPE(kernels->u, PS_TYPE_S32, false);
+    PS_ASSERT_VECTOR_TYPE(kernels->v, PS_TYPE_S32, false);
+    PS_ASSERT_VECTOR_TYPE(kernels->widths, PS_TYPE_F32, false);
+    PS_ASSERT_ARRAY_NON_NULL(kernels->preCalc, false);
+    PS_ASSERT_INT_NONNEGATIVE(start, false);
+    PS_ASSERT_INT_NONNEGATIVE(size, false);
+
+    int numNew = PS_SQR(2 * size + 1);  // Number of new kernel parameters to add
+
+    // Ensure the sizes match
+    kernels->widths = psVectorRealloc(kernels->widths, start + numNew);
+    kernels->u = psVectorRealloc(kernels->u, start + numNew);
+    kernels->v = psVectorRealloc(kernels->v, start + numNew);
+    kernels->preCalc = psArrayRealloc(kernels->preCalc, start + numNew);
+    kernels->inner = start;
+
+    // Generate a set of kernels for each (u,v)
+    for (int v = - size, index = start; v <= size; v++) {
+        for (int u = - size; u <= size; u++, index++) {
+            kernels->widths->data.F32[index] = NAN;
+            kernels->u->data.S32[index] = u;
+            kernels->v->data.S32[index] = v;
+            kernels->preCalc->data[index] = NULL;
+
+            psTrace("psModules.imcombine", 7, "Kernel %d: %d %d\n", index, u, v);
+        }
+    }
+
+    return true;
+}
+
+pmSubtractionKernels *p_pmSubtractionKernelsRawISIS(int size, int spatialOrder,
+                                                  const psVector *fwhms, const psVector *orders)
+{
+    PS_ASSERT_VECTOR_NON_NULL(fwhms, NULL);
+    PS_ASSERT_VECTOR_TYPE(fwhms, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(orders, NULL);
+    PS_ASSERT_VECTOR_TYPE(orders, PS_TYPE_S32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(fwhms, orders, NULL);
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+
+    int numGaussians = fwhms->n;       // Number of Gaussians
+
+    int num = 0;                        // Number of basis functions
+    psString params = NULL;             // List of parameters
+    for (int i = 0; i < numGaussians; i++) {
+        int gaussOrder = orders->data.S32[i]; // Polynomial order to apply to Gaussian
+        psStringAppend(&params, "(%.2f,%d)", fwhms->data.F32[i], orders->data.S32[i]);
+        num += (gaussOrder + 1) * (gaussOrder + 2) / 2;
+    }
+
+    pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(num, PM_SUBTRACTION_KERNEL_ISIS,
+                                                              size, spatialOrder); // The kernels
+    psStringAppend(&kernels->description, "ISIS(%d,%s,%d)", size, params, spatialOrder);
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "ISIS kernel: %s,%d --> %d elements",
+             params, spatialOrder, num);
+    psFree(params);
+
+    // Set the kernel parameters
+    for (int i = 0, index = 0; i < numGaussians; i++) {
+        float sigma = fwhms->data.F32[i] / (2.0 * sqrtf(2.0 * logf(2.0))); // Gaussian sigma
+        float norm = 1.0 / (M_2_PI * sqrtf(sigma)); // Normalisation for Gaussian
+        // Iterate over (u,v) order
+        for (int uOrder = 0; uOrder <= orders->data.S32[i]; uOrder++) {
+            for (int vOrder = 0; vOrder <= orders->data.S32[i] - uOrder; vOrder++, index++) {
+                // Set the pre-calculated kernel
+                psKernel *preCalc = psKernelAlloc(-size, size, -size, size);
+                double sum = 0.0;       // Normalisation
+                for (int v = -size; v <= size; v++) {
+                    for (int u = -size; u <= size; u++) {
+                        sum += preCalc->kernel[v][u] = norm * power(u, uOrder) * power(v, vOrder) *
+                            expf(-0.5 * (PS_SQR(u) + PS_SQR(v)) / PS_SQR(sigma));
+                    }
+                }
+
+                // Normalise sum of kernel component to unity for even functions
+                if (uOrder % 2 == 0 && vOrder % 2 == 0) {
+                    for (int v = -size; v <= size; v++) {
+                        for (int u = -size; u <= size; u++) {
+                            preCalc->kernel[v][u] = preCalc->kernel[v][u] / sum;
+                        }
+                    }
+                }
+
+                kernels->widths->data.F32[index] = fwhms->data.F32[i];
+                kernels->u->data.S32[index] = uOrder;
+                kernels->v->data.S32[index] = vOrder;
+                if (kernels->preCalc->data[index]) {
+                    psFree(kernels->preCalc->data[index]);
+                }
+                kernels->preCalc->data[index] = preCalc;
+
+                psTrace("psModules.imcombine", 7, "Kernel %d: %f %d %d\n", index,
+                        fwhms->data.F32[i], uOrder, vOrder);
+            }
+        }
+    }
+
+    kernels->subIndex = -1;
+
+    return kernels;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+pmSubtractionKernels *pmSubtractionKernelsAlloc(int numBasisFunctions, pmSubtractionKernelsType type,
+                                                int size, int spatialOrder)
+{
+    pmSubtractionKernels *kernels = psAlloc(sizeof(pmSubtractionKernels)); // Kernels, to return
+    psMemSetDeallocator(kernels, (psFreeFunc)subtractionKernelsFree);
+
+    kernels->type = type;
+    kernels->description = NULL;
+    kernels->num = numBasisFunctions;
+    kernels->u = psVectorAlloc(numBasisFunctions, PS_TYPE_S32);
+    kernels->v = psVectorAlloc(numBasisFunctions, PS_TYPE_S32);
+    kernels->widths = psVectorAlloc(numBasisFunctions, PS_TYPE_F32);
+    kernels->preCalc = psArrayAlloc(numBasisFunctions);
+    kernels->uStop = NULL;
+    kernels->vStop = NULL;
+    kernels->subIndex = 0;
+    kernels->size = size;
+    kernels->inner = 0;
+    kernels->spatialOrder = spatialOrder;
+    kernels->bgOrder = 0;
+
+    return kernels;
+}
+
+pmSubtractionKernels *pmSubtractionKernelsPOIS(int size, int spatialOrder)
+{
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+
+    int num = PS_SQR(2 * size + 1); // Number of basis functions
+
+    pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(num, PM_SUBTRACTION_KERNEL_POIS,
+                                                              size, spatialOrder); // The kernels
+    psStringAppend(&kernels->description, "POIS(%d,%d)", size, spatialOrder);
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "POIS kernel: %d,%d --> %d elements",
+             size, spatialOrder, num);
+
+    if (!p_pmSubtractionKernelsAddGrid(kernels, 0, size)) {
+        psAbort("Should never get here.");
+    }
+
+    kernels->subIndex = num / 2;
+    assert(kernels->u->data.S32[kernels->subIndex] == 0 &&
+           kernels->v->data.S32[kernels->subIndex] == 0);
+
+    return kernels;
+}
+
+
+pmSubtractionKernels *pmSubtractionKernelsISIS(int size, int spatialOrder,
+                                               const psVector *fwhms, const psVector *orders)
+{
+    pmSubtractionKernels *kernels = p_pmSubtractionKernelsRawISIS(size, spatialOrder,
+                                                                  fwhms, orders); // Kernels
+    if (!kernels) {
+        return NULL;
+    }
+
+    // Subtract a reference kernel from all the others to maintain flux scaling
+    if (spatialOrder > 0) {
+        int subIndex = 0;                   // Index of kernel to subtract from others
+        psKernel *subtract = kernels->preCalc->data[subIndex]; // Kernel to subtract from others
+        int numGaussians = fwhms->n;       // Number of Gaussians
+        for (int i = 0, index = 0; i < numGaussians; i++) {
+            for (int uOrder = 0; uOrder <= orders->data.S32[i]; uOrder++) {
+                for (int vOrder = 0; vOrder <= orders->data.S32[i] - uOrder; vOrder++, index++) {
+                    if (uOrder % 2 == 0 && vOrder % 2 == 0 && index != subIndex) {
+                        psKernel *kernel = kernels->preCalc->data[index]; // Kernel to subtract
+                        psBinaryOp(kernel->image, kernel->image, "-", subtract->image);
+                    }
+                }
+            }
+        }
+    }
+
+    if (psTraceGetLevel("psModules.imcombine.kernel") >= 10) {
+        for (int i = 0; i < kernels->num; i++) {
+            psKernel *kernel = kernels->preCalc->data[i]; // Kernel of interest
+            psString kernelName = NULL;
+            psStringAppend(&kernelName, "kernel%03d.fits", i);
+            psFits *kernelFile = psFitsOpen(kernelName, "w");
+            psFree(kernelName);
+            psFitsWriteImage(kernelFile, NULL, kernel->image, 0, NULL);
+            psFitsClose(kernelFile);
+        }
+    }
+
+    return kernels;
+}
+
+/// Generate SPAM kernels
+pmSubtractionKernels *pmSubtractionKernelsSPAM(int size, ///< Half-size of the kernel
+                                               int spatialOrder, ///< Order of spatial variations
+                                               int inner, ///< Inner radius to preserve unbinned
+                                               int binning ///< Kernel binning factor
+    )
+{
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(inner, NULL);
+    PS_ASSERT_INT_LARGER_THAN(size, inner, NULL);
+    PS_ASSERT_INT_POSITIVE(binning, NULL);
+
+    // The outer region should be divisible by the "binning"; otherwise allocate remainder to the inner region
+    int numOuter = (size - inner) / binning; // Number of summed pixels in the outer region
+    int numInner = inner + (size - inner) % binning; // Number of pixels in the inner region
+    assert(numOuter * binning + numInner == size);
+    int numTotal = numOuter + numInner; // Total number of summed pixels
+
+    psTrace("psModules.imcombine", 3, "Inner: %d Outer: %d\n", numInner, numOuter);
+
+    int num = PS_SQR(2 * numTotal + 1); // Number of basis functions
+
+    psTrace("psModules.imcombine", 3, "Number of basis functions: %d\n", num);
+
+    pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(num, PM_SUBTRACTION_KERNEL_SPAM,
+                                                              size, spatialOrder); // The kernels
+    psStringAppend(&kernels->description, "SPAM(%d,%d,%d,%d)", size, inner, binning, spatialOrder);
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "SPAM kernel: %d,%d,%d,%d --> %d elements",
+             size, inner, binning, spatialOrder, num);
+
+    kernels->uStop = psVectorAlloc(num, PS_TYPE_F32);
+    kernels->vStop = psVectorAlloc(num, PS_TYPE_F32);
+
+    psVector *locations = psVectorAlloc(2 * numTotal + 1, PS_TYPE_S32); // Locations for each kernel element
+    psVector *widths = psVectorAlloc(2 * numTotal + 1, PS_TYPE_S32); // Widths for each kernel element
+    locations->data.S32[numTotal] = 0;
+    widths->data.S32[numTotal] = 0;
+    for (int i = 1; i <= numInner; i++) {
+        locations->data.S32[numTotal + i] = i;
+        widths->data.S32[numTotal + i] = 0;
+        locations->data.S32[numTotal - i] = - i;
+        widths->data.S32[numTotal - i] = 0;
+    }
+    for (int i = numInner + 1; i <= numTotal; i++) {
+        locations->data.S32[numTotal + i] = locations->data.S32[numTotal + i - 1] +
+            widths->data.S32[numTotal + i - 1] + 1;
+        widths->data.S32[numTotal + i] = binning - 1;
+        locations->data.S32[numTotal - i] = locations->data.S32[numTotal - i + 1] - binning;
+        widths->data.S32[numTotal - i] = binning - 1;
+    }
+
+    if (psTraceGetLevel("psModules.imcombine") >= 10) {
+        for (int i = 0; i < 2 * numTotal + 1; i++) {
+            psTrace("psModules.imcombine", 10, "%d: %d -> %d\n", i, locations->data.S32[i],
+                    locations->data.S32[i] + widths->data.S32[i]);
+        }
+    }
+
+    // Set the kernel parameters
+    for (int i = - numTotal, index = 0; i <= numTotal; i++) {
+        int u = locations->data.S32[numTotal + i]; // Location of pixel
+        int uStop = u + widths->data.S32[numTotal + i]; // Width of pixel
+
+        for (int j = - numTotal; j <= numTotal; j++, index++) {
+            int v = locations->data.S32[numTotal + j]; // Location of pixel
+            int vStop = v + widths->data.S32[numTotal + j]; // Width of pixel
+
+            kernels->u->data.S32[index] = u;
+            kernels->v->data.S32[index] = v;
+            kernels->uStop->data.S32[index] = uStop;
+            kernels->vStop->data.S32[index] = vStop;
+
+            psTrace("psModules.imcombine", 7, "Kernel %d: %d %d %d %d\n", index,
+                    u, uStop, v, vStop);
+        }
+    }
+
+    kernels->subIndex = num / 2;
+    assert(kernels->u->data.S32[kernels->subIndex] == 0 &&
+           kernels->v->data.S32[kernels->subIndex] == 0 &&
+           kernels->uStop->data.S32[kernels->subIndex] == 0 &&
+           kernels->vStop->data.S32[kernels->subIndex] == 0);
+
+    psFree(locations);
+    psFree(widths);
+
+    return kernels;
+}
+
+
+/// Generate FRIES kernels
+pmSubtractionKernels *pmSubtractionKernelsFRIES(int size, ///< Half-size of the kernel
+                                                int spatialOrder, ///< Order of spatial variations
+                                                int inner ///< Inner radius to preserve unbinned
+    )
+{
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(inner, NULL);
+    PS_ASSERT_INT_LARGER_THAN(size, inner, NULL);
+
+    int fibNum = 0;                     // Number of Fibonacci values
+    int fibLast = 1, fibTotal = 2;      // Fibonacci sequence
+    while (fibTotal < size - inner) {
+        int temp = fibTotal;
+        fibTotal += fibLast;
+        fibLast = temp;
+        fibNum++;
+    }
+
+    int numInner = inner;               // Number of pixels in the inner region
+    int numOuter = fibNum;              // Number of summed pixels in the outer region
+    int numTotal = numOuter + numInner; // Total number of summed pixels
+
+    psTrace("psModules.imcombine", 3, "Inner: %d Outer: %d\n", numInner, numOuter);
+
+    int num = PS_SQR(2 * numTotal + 1); // Number of basis functions
+
+    psTrace("psModules.imcombine", 3, "Number of basis functions: %d\n", num);
+
+    pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(num, PM_SUBTRACTION_KERNEL_FRIES,
+                                                              size, spatialOrder); // The kernels
+    psStringAppend(&kernels->description, "FRIES(%d,%d,%d)", size, inner, spatialOrder);
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "FRIES kernel: %d,%d,%d --> %d elements",
+             size, inner, spatialOrder, num);
+
+    kernels->uStop = psVectorAlloc(num, PS_TYPE_F32);
+    kernels->vStop = psVectorAlloc(num, PS_TYPE_F32);
+
+    psVector *start = psVectorAlloc(2 * numTotal + 1, PS_TYPE_S32);
+    psVector *stop = psVectorAlloc(2 * numTotal + 1, PS_TYPE_S32);
+    start->data.S32[numTotal] = 0;
+    stop->data.S32[numTotal] = 0;
+    for (int i = 1; i <= numInner; i++) {
+        start->data.S32[numTotal + i] = i;
+        stop->data.S32[numTotal + i] = i;
+        start->data.S32[numTotal - i] = -i;
+        stop->data.S32[numTotal - i] = -i;
+    }
+    for (int i = numInner + 1, fibLast = 1, fib = 2, temp; i <= numTotal;
+         i++, fib = (temp = fib) + fibLast, fibLast = temp) {
+        start->data.S32[numTotal + i] = stop->data.S32[numTotal + i - 1] + 1;
+        stop->data.S32[numTotal + i] = PS_MIN(start->data.S32[numTotal + i] + fib - 1, size);
+        start->data.S32[numTotal - i] = - stop->data.S32[numTotal + i];
+        stop->data.S32[numTotal - i] = - start->data.S32[numTotal + i];
+    }
+
+    if (psTraceGetLevel("psModules.imcombine") >= 10) {
+        for (int i = 0; i < 2 * numTotal + 1; i++) {
+            psTrace("psModules.imcombine", 10, "%d: %d -> %d\n", i, start->data.S32[i], stop->data.S32[i]);
+        }
+    }
+
+    // Set the kernel parameters
+    for (int i = - numTotal, index = 0; i <= numTotal; i++) {
+        int u = start->data.S32[numTotal + i]; // Location of pixel
+        int uStop = stop->data.S32[numTotal + i]; // Width of pixel
+        for (int j = - numTotal; j <= numTotal; j++, index++) {
+            int v = start->data.S32[numTotal + j]; // Location of pixel
+            int vStop = stop->data.S32[numTotal + j]; // Width of pixel
+
+            kernels->u->data.S32[index] = u;
+            kernels->v->data.S32[index] = v;
+            kernels->uStop->data.S32[index] = uStop;
+            kernels->vStop->data.S32[index] = vStop;
+
+            psTrace("psModules.imcombine", 7, "Kernel %d: %d %d %d %d\n", index,
+                    u, uStop, v, vStop);
+        }
+    }
+
+    kernels->subIndex = num / 2;
+    assert(kernels->u->data.S32[kernels->subIndex] == 0 &&
+           kernels->v->data.S32[kernels->subIndex] == 0 &&
+           kernels->uStop->data.S32[kernels->subIndex] == 0 &&
+           kernels->vStop->data.S32[kernels->subIndex] == 0);
+
+    psFree(start);
+    psFree(stop);
+
+    return kernels;
+}
+
+// Grid United with Normal Kernel
+pmSubtractionKernels *pmSubtractionKernelsGUNK(int size, int spatialOrder, const psVector *fwhms,
+                                               const psVector *orders, int inner)
+{
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(fwhms, NULL);
+    PS_ASSERT_VECTOR_TYPE(fwhms, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(orders, NULL);
+    PS_ASSERT_VECTOR_TYPE(orders, PS_TYPE_S32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(fwhms, orders, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(inner, NULL);
+    PS_ASSERT_INT_LESS_THAN(inner, size, NULL);
+
+    pmSubtractionKernels *kernels = p_pmSubtractionKernelsRawISIS(size, spatialOrder,
+                                                                  fwhms, orders); // Kernels
+    psStringPrepend(&kernels->description, "GUNK=");
+    psStringAppend(&kernels->description, "+POIS(%d,%d)", inner, spatialOrder);
+
+    // Subtract unity from the kernels to maintain photometric flux scaling
+    int numGaussians = fwhms->n;       // Number of Gaussians
+    if (spatialOrder > 0) {
+        for (int i = 0, index = 0; i < numGaussians; i++) {
+            for (int uOrder = 0; uOrder <= orders->data.S32[i]; uOrder++) {
+                for (int vOrder = 0; vOrder <= orders->data.S32[i] - uOrder; vOrder++, index++) {
+                    if (uOrder % 2 == 0 && vOrder % 2 == 0) {
+                        psKernel *kernel = kernels->preCalc->data[index]; // Kernel to subtract
+                        kernel->kernel[0][0] -= 1.0;
+                    }
+                }
+            }
+        }
+    }
+
+    int numISIS = kernels->num;         // Number of ISIS kernels
+    int numInner = PS_SQR(2 * inner + 1); // Number of inner kernel elements
+
+    if (!p_pmSubtractionKernelsAddGrid(kernels, numISIS, inner)) {
+        psAbort("Should never get here.");
+    }
+
+    kernels->subIndex = numInner/2 + numISIS;
+    assert(kernels->u->data.S32[kernels->subIndex] == 0 &&
+           kernels->v->data.S32[kernels->subIndex] == 0);
+
+    return kernels;
+}
+
+// RINGS --- just what it says
+pmSubtractionKernels *pmSubtractionKernelsRINGS(int size, int spatialOrder, int inner, int ringsOrder)
+{
+    PS_ASSERT_INT_POSITIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(inner, NULL);
+    PS_ASSERT_INT_LESS_THAN(inner, size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(ringsOrder, NULL);
+
+    int fibNum = 0;                     // Number of Fibonacci values
+    {
+        int fibIndex = 1, fibIndexMinus1 = 0; // Fibonnacci parameters
+        int radius = inner;
+        while (radius < size) {
+            radius++;
+            int fibNew = fibIndex + fibIndexMinus1;
+            fibIndexMinus1 = fibIndex;
+            fibIndex = fibNew;
+            radius += fibIndex;
+            fibNum++;
+            printf("%d ", radius);
+        }
+        printf("\n");
+    }
+
+    int numInner = inner;               // Number of pixels in the inner region
+    int numOuter = fibNum;              // Number of summed pixels in the outer region
+
+    int numRings = numOuter + numInner; // Number of rings (not including the central pixel)
+    int numPoly = (ringsOrder + 1) * (ringsOrder + 2) / 2; // Number of polynomial variants of each ring
+
+    int num = numRings * numPoly + 1; // Total number of basis functions
+
+    pmSubtractionKernels *kernels = pmSubtractionKernelsAlloc(num, PM_SUBTRACTION_KERNEL_RINGS,
+                                                              size, spatialOrder); // The kernels
+    psStringAppend(&kernels->description, "RINGS(%d,%d,%d,%d)", size, inner, ringsOrder, spatialOrder);
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "RINGS kernel: %d,%d,%d,%d --> %d elements",
+             size, inner, ringsOrder, spatialOrder, num);
+
+    // Set the Gaussian kernel parameters
+    int fibIndex = 1, fibIndexMinus1 = 0; // Fibonnacci parameters
+    int radiusLast = 0;                 // Last radius
+    for (int i = 0, index = 0; i < numRings + 1; i++) {
+
+        float lower2;                   // Lower limit of radius^2
+        float upper2;                   // Upper limit of radius^2
+        if (i == 0) {
+            lower2 = 0;
+            upper2 = PS_SQR(0.5);
+        } else if (i <= numInner) {
+            // A ring every pixel width
+            float radius = i;
+            lower2 = PS_SQR(radius - 0.5);
+            upper2 = PS_SQR(radius + 0.5);
+            radiusLast = i;
+        } else {
+            // Rings Fibonacci distributed (2, 3, 5...)
+            int fibNew = fibIndex + fibIndexMinus1;
+            fibIndexMinus1 = fibIndex;
+            fibIndex = fibNew;
+
+            float radiusLower = radiusLast + 1;
+            radiusLast = radiusLower + fibIndex;
+            float radiusUpper = radiusLast;
+
+            lower2 = PS_SQR(radiusLower - 0.5);
+            upper2 = PS_SQR(radiusUpper + 0.5);
+        }
+
+        psTrace("psModules.imcombine", 8, "Radius limits: %f --> %f\n", sqrtf(lower2), sqrtf(upper2));
+
+        // Iterate over (u,v) order
+        for (int uOrder = 0; uOrder <= (i == 0 ? 0 : ringsOrder); uOrder++) {
+            for (int vOrder = 0; vOrder <= (i == 0 ? 0 : ringsOrder - uOrder); vOrder++, index++) {
+
+                psArray *data = psArrayAlloc(3); // Container for data
+                psVector *uCoords = data->data[0] = psVectorAllocEmpty(RINGS_BUFFER, PS_TYPE_S32); // u coords
+                psVector *vCoords = data->data[1] = psVectorAllocEmpty(RINGS_BUFFER, PS_TYPE_S32); // v coords
+                psVector *poly = data->data[2] = psVectorAllocEmpty(RINGS_BUFFER, PS_TYPE_F32); // Polynomial
+
+                if (i == 0) {
+                    // Central pixel is easy
+                    uCoords->data.S32[0] = vCoords->data.S32[0] = 0;
+                    poly->data.F32[0] = 1.0;
+                    uCoords->n = vCoords->n = poly->n = 1;
+                    radiusLast = 0;
+                } else {
+                    int j = 0;          // Index for data
+                    double norm = 0.0;  // Normalisation
+                    for (int v = -size; v <= size; v++) {
+                        int v2 = PS_SQR(v);   // Square of v
+                        float vPoly = power(v, vOrder); // Value of v^vOrder
+
+                        for (int u = -size; u <= size; u++) {
+                            int u2 = PS_SQR(u); // Square of u
+                            int distance2 = u2 + v2; // Distance from the centre
+                            if (distance2 > lower2 && distance2 < upper2) {
+                                float uPoly = power(u, uOrder); // Value of u^uOrder
+
+                                float polyVal = uPoly * vPoly; // Value of polynomial
+                                if (polyVal != 0) { // No point adding it otherwise
+                                    uCoords->data.S32[j] = u;
+                                    vCoords->data.S32[j] = v;
+                                    poly->data.F32[j] = polyVal;
+                                    norm += polyVal;
+
+                                    psVectorExtend(uCoords, RINGS_BUFFER, 1);
+                                    psVectorExtend(vCoords, RINGS_BUFFER, 1);
+                                    psVectorExtend(poly, RINGS_BUFFER, 1);
+                                    psTrace("psModules.imcombine", 9, "u = %d, v = %d, poly = %f\n",
+                                            u, v, poly->data.F32[j]);
+                                    j++;
+                                }
+                            }
+                        }
+                    }
+                    // Normalise kernel component to unit sum
+                    if (uOrder % 2 == 0 && vOrder % 2 == 0) {
+                        psBinaryOp(poly, poly, "*", psScalarAlloc(1.0 / norm, PS_TYPE_F32));
+                        // Add subtraction of 0,0 component to preserve photometric scaling
+                        uCoords->data.S32[j] = 0;
+                        vCoords->data.S32[j] = 0;
+                        poly->data.F32[j] = -1.0;
+                        psVectorExtend(uCoords, RINGS_BUFFER, 1);
+                        psVectorExtend(vCoords, RINGS_BUFFER, 1);
+                        psVectorExtend(poly, RINGS_BUFFER, 1);
+                    } else {
+                        norm = powf(size, uOrder) * powf(size, vOrder);
+                        psBinaryOp(poly, poly, "*", psScalarAlloc(1.0 / norm, PS_TYPE_F32));
+                    }
+                }
+
+                psTrace("psModules.imcombine", 8, "%ld pixels in kernel\n", uCoords->n);
+
+                kernels->preCalc->data[index] = data;
+                kernels->u->data.S32[index] = uOrder;
+                kernels->v->data.S32[index] = vOrder;
+
+                psTrace("psModules.imcombine", 7, "Kernel %d: %d %d %d\n", index,
+                        i, uOrder, vOrder);
+            }
+        }
+    }
+
+    kernels->subIndex = 0;
+
+    return kernels;
+}
+
+pmSubtractionKernels *pmSubtractionKernelsGenerate(pmSubtractionKernelsType type, int size, int spatialOrder,
+                                                   const psVector *fwhms, const psVector *orders, int inner,
+                                                   int binning, int ringsOrder)
+{
+    switch (type) {
+      case PM_SUBTRACTION_KERNEL_POIS:
+        return pmSubtractionKernelsPOIS(size, spatialOrder);
+      case PM_SUBTRACTION_KERNEL_ISIS:
+        return pmSubtractionKernelsISIS(size, spatialOrder, fwhms, orders);
+      case PM_SUBTRACTION_KERNEL_SPAM:
+        return pmSubtractionKernelsSPAM(size, spatialOrder, inner, binning);
+      case PM_SUBTRACTION_KERNEL_FRIES:
+        return pmSubtractionKernelsFRIES(size, spatialOrder, inner);
+      case PM_SUBTRACTION_KERNEL_GUNK:
+        return pmSubtractionKernelsGUNK(size, spatialOrder, fwhms, orders, inner);
+      case PM_SUBTRACTION_KERNEL_RINGS:
+        return pmSubtractionKernelsRINGS(size, spatialOrder, inner, ringsOrder);
+      default:
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unknown kernel type: %x", type);
+        return NULL;
+    }
+}
+
+
+pmSubtractionKernelsType pmSubtractionKernelsTypeFromString(const char *type)
+{
+    if (strcasecmp(type, "POIS") == 0) {
+        return PM_SUBTRACTION_KERNEL_POIS;
+    }
+    if (strcasecmp(type, "ISIS") == 0) {
+        return PM_SUBTRACTION_KERNEL_ISIS;
+    }
+    if (strcasecmp(type, "SPAM") == 0) {
+        return PM_SUBTRACTION_KERNEL_SPAM;
+    }
+    if (strcasecmp(type, "FRIES") == 0) {
+        return PM_SUBTRACTION_KERNEL_FRIES;
+    }
+    if (strcasecmp(type, "GUNK") == 0) {
+        return PM_SUBTRACTION_KERNEL_GUNK;
+    }
+    if (strcasecmp(type, "RINGS") == 0) {
+        return PM_SUBTRACTION_KERNEL_RINGS;
+    }
+
+    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised kernel type: %s", type);
+    return PM_SUBTRACTION_KERNEL_NONE;
+}
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionKernels.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionKernels.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionKernels.h	(revision 15358)
@@ -0,0 +1,113 @@
+#ifndef PM_SUBTRACTION_KERNELS_H
+#define PM_SUBTRACTION_KERNELS_H
+
+#include <pslib.h>
+
+/// Type of subtraction kernel
+typedef enum {
+    PM_SUBTRACTION_KERNEL_NONE,         ///< Nothing --- an error
+    PM_SUBTRACTION_KERNEL_POIS,         ///< Pan-STARRS Optimal Image Subtraction --- delta functions
+    PM_SUBTRACTION_KERNEL_ISIS,         ///< Traditional kernel --- gaussians modified by polynomials
+    PM_SUBTRACTION_KERNEL_SPAM,         ///< Summed Pixels for Advanced Matching --- summed delta functions
+    PM_SUBTRACTION_KERNEL_FRIES,        ///< Fibonacci Radius Increases Excellence of Subtraction
+    PM_SUBTRACTION_KERNEL_GUNK,         ///< Grid United with Normal Kernel --- POIS and ISIS hybrid
+    PM_SUBTRACTION_KERNEL_RINGS,        ///< Rings Instead of the Normal Gaussian Subtraction
+} pmSubtractionKernelsType;
+
+/// Kernels specification
+typedef struct {
+    pmSubtractionKernelsType type;      ///< Type of kernels --- allowing the use of multiple kernels
+    psString description;               ///< Description of the kernel parameters
+    long num;                           ///< Number of kernel components (not including the spatial ones)
+    psVector *u, *v;                    ///< Offset (for POIS) or polynomial order (for ISIS)
+    psVector *widths;                   ///< Gaussian FWHMs (ISIS) or ring radius (RINGS)
+    psVector *uStop, *vStop;            ///< Width of kernel element (SPAM,FRIES only)
+    int subIndex;                       ///< Index of kernel to be subtracted (to maintain flux conservation)
+    psArray *preCalc;                   ///< Array of images containing pre-calculated kernel (for ISIS)
+    int size;                           ///< The half-size of the kernel
+    int inner;                          ///< The size of an inner region
+    int spatialOrder;                   ///< The spatial order of the kernels
+    int bgOrder;                        ///< The order for the background fitting
+} pmSubtractionKernels;
+
+/// Generate a delta-function grid for subtraction kernels (like the POIS kernel)
+bool p_pmSubtractionKernelsAddGrid(pmSubtractionKernels *kernels, ///< The subtraction kernels to append to
+                                   int start, ///< Index at which to start appending
+                                   int size ///< Half-size of the grid
+    );
+
+/// General allocator for pmSubtractionKernels
+///
+/// Unlike the functions for the specific kernel type, this function does not set up the basis functions, but
+/// merely allocates space for their storage.
+pmSubtractionKernels *pmSubtractionKernelsAlloc(int numBasisFunctions, ///< Number of basis functions
+                                                pmSubtractionKernelsType type, ///< Kernel type
+                                                int size, ///< Half-size of kernel
+                                                int spatialOrder ///< Order of spatial variations
+    );
+
+/// Generate POIS kernels
+pmSubtractionKernels *pmSubtractionKernelsPOIS(int size, ///< Half-size of the kernel (in both dims)
+                                               int spatialOrder ///< Order of spatial variations
+    );
+
+/// Generate ISIS kernels without the flux scaling built in
+pmSubtractionKernels *p_pmSubtractionKernelsRawISIS(int size, ///< Half-size of the kernel
+                                                    int spatialOrder, ///< Order of spatial variations
+                                                    const psVector *fwhms, ///< Gaussian FWHMs
+                                                    const psVector *orders ///< Polynomial order of gaussians
+    );
+
+/// Generate ISIS kernels
+pmSubtractionKernels *pmSubtractionKernelsISIS(int size, ///< Half-size of the kernel
+                                               int spatialOrder, ///< Order of spatial variations
+                                               const psVector *fwhms, ///< Gaussian FWHMs
+                                               const psVector *orders ///< Polynomial order of gaussians
+                                               );
+
+/// Generate SPAM kernels
+pmSubtractionKernels *pmSubtractionKernelsSPAM(int size, ///< Half-size of the kernel
+                                               int spatialOrder, ///< Order of spatial variations
+                                               int inner, ///< Inner radius to preserve unbinned
+                                               int binning ///< Kernel binning factor
+    );
+
+/// Generate FRIES kernels
+pmSubtractionKernels *pmSubtractionKernelsFRIES(int size, ///< Half-size of the kernel
+                                                int spatialOrder, ///< Order of spatial variations
+                                                int inner ///< Inner radius to preserve unbinned
+    );
+
+/// Generate GUNK kernels
+pmSubtractionKernels *pmSubtractionKernelsGUNK(int size, ///< Half-size of the kernel
+                                               int spatialOrder, ///< Order of spatial variations
+                                               const psVector *fwhms, ///< Gaussian FWHMs
+                                               const psVector *orders, ///< Polynomial order of gaussians
+                                               int inner ///< Inner radius containing grid of delta functions
+    );
+
+/// Generate RINGS kernels
+pmSubtractionKernels *pmSubtractionKernelsRINGS(int size, ///< Half-size of the kernel
+                                                int spatialOrder, ///< Order of spatial variations
+                                                int inner, ///< Inner radius to preserve unbinned
+                                                int ringsOrder ///< Polynomial order
+    );
+
+
+/// Generate a kernel of a specified type
+pmSubtractionKernels *pmSubtractionKernelsGenerate(pmSubtractionKernelsType type, ///< Kernel type
+                                                   int size, ///< Half-size of the kernel
+                                                   int spatialOrder, ///< Order of spatial variations
+                                                   const psVector *fwhms, ///< Gaussian FWHMs
+                                                   const psVector *orders, ///< Polynomial order of gaussians
+                                                   int inner, ///< Inner radius to preserve unbinned
+                                                   int binning, ///< Kernel binning factor
+                                                   int ringsOrder ///< Polynomial order for RINGS
+    );
+
+/// Return the appropriate type from a string
+pmSubtractionKernelsType pmSubtractionKernelsTypeFromString(const char *string // String name for kernel type
+    );
+
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionMatch.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionMatch.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionMatch.c	(revision 15358)
@@ -0,0 +1,482 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmSubtractionParams.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionMatch.h"
+
+
+static bool useFFT = true;              // Do convolutions using FFT
+
+
+//#define TESTING
+
+// Output memory usage information
+static void memCheck(const char *where)
+{
+#ifdef TESTING
+    psMemBlock **leaks = NULL;
+    int numLeaks = psMemCheckLeaks(0, &leaks, NULL, true);
+    size_t largestSize = 0;
+    psMemId largest = 0;
+    size_t totalSize = 0;
+    for (int i = 0; i < numLeaks; i++) {
+        psMemBlock *mb = leaks[i];
+        totalSize += mb->userMemorySize;
+        if (mb->userMemorySize > largestSize) {
+            largestSize = mb->userMemorySize;
+            largest = mb->id;
+        }
+    }
+    psFree(leaks);
+    fprintf(stderr, "%s:\n", where);
+    fprintf(stderr, "    Memory in use: %zd\n", totalSize);
+    fprintf(stderr, "    Largest block: %ld\n", largest);
+    fprintf(stderr, "    sbrk(): %zd\n", (size_t)sbrk(0));
+#endif
+    return;
+}
+
+
+static bool getStamps(pmSubtractionStampList **stamps, // Stamps to read
+                      const pmReadout *reference, // Reference readout
+                      const pmReadout *input, // Input readout, or NULL to generate fake stamps
+                      const psImage *subMask, // Mask for subtraction, or NULL
+                      psImage *weight,  // Weight map
+                      const psRegion *region, // Region of interest, or NULL
+                      float threshold,  // Threshold for stamp finding
+                      float stampSpacing, // Spacing between stamps
+                      float targetWidth,// Target width for fake stamps
+                      int size,         // Kernel half-size
+                      int footprint     // Convolution footprint for stamps
+    )
+{
+    psTrace("psModules.imcombine", 3, "Finding stamps...\n");
+    *stamps = pmSubtractionStampsFind(*stamps, reference->image, subMask, region, threshold, stampSpacing);
+    if (!*stamps) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find stamps.");
+        return false;
+    }
+
+    memCheck("  find stamps");
+
+    if (!input && !pmSubtractionStampsGenerate(*stamps, targetWidth, footprint, size)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate target stamps.");
+        return false;
+    }
+
+    memCheck("   generate stamps");
+
+    psTrace("psModules.imcombine", 3, "Extracting stamps...\n");
+    if (!pmSubtractionStampsExtract(*stamps, reference->image, input ? input->image : NULL,
+                                    weight, footprint, size)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to extract stamps.");
+        return false;
+    }
+
+    memCheck("   extract stamps");
+
+    return true;
+}
+
+
+
+
+
+bool pmSubtractionMatch(pmReadout *convolved, const pmReadout *reference, const pmReadout *input,
+                        int footprint, float regionSize, float stampSpacing, float threshold,
+                        const psArray *sources, const char *stampsName, float targetWidth,
+                        pmSubtractionKernelsType type, int size, int spatialOrder,
+                        const psVector *isisWidths, const psVector *isisOrders,
+                        int inner, int ringsOrder, int binning, bool optimum, const psVector *optFWHMs,
+                        int optOrder, float optThreshold, int iter, float rej, psMaskType maskBad,
+                        psMaskType maskBlank, float badFrac)
+{
+    PS_ASSERT_PTR_NON_NULL(convolved, false);
+    PS_ASSERT_PTR_NON_NULL(reference, false);
+    PS_ASSERT_IMAGE_NON_NULL(reference->image, false);
+    PS_ASSERT_IMAGE_TYPE(reference->image, PS_TYPE_F32, false);
+    if (reference->mask) {
+        PS_ASSERT_IMAGE_NON_NULL(reference->mask, false);
+        PS_ASSERT_IMAGE_TYPE(reference->mask, PS_TYPE_MASK, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(reference->mask, reference->image, false);
+    }
+    if (reference->weight) {
+        PS_ASSERT_IMAGE_NON_NULL(reference->weight, false);
+        PS_ASSERT_IMAGE_TYPE(reference->weight, PS_TYPE_F32, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(reference->weight, reference->image, false);
+    }
+    if (input) {
+        PS_ASSERT_IMAGE_NON_NULL(input->image, false);
+        PS_ASSERT_IMAGE_TYPE(input->image, PS_TYPE_F32, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(input->image, reference->image, false);
+        if (input->mask) {
+            PS_ASSERT_IMAGE_NON_NULL(input->mask, false);
+            PS_ASSERT_IMAGE_TYPE(input->mask, PS_TYPE_MASK, false);
+            PS_ASSERT_IMAGES_SIZE_EQUAL(input->mask, reference->image, false);
+        }
+        if (input->weight) {
+            PS_ASSERT_IMAGE_NON_NULL(input->weight, false);
+            PS_ASSERT_IMAGE_TYPE(input->weight, PS_TYPE_F32, false);
+            PS_ASSERT_IMAGES_SIZE_EQUAL(input->weight, reference->image, false);
+        }
+    } else if (!stampsName && !sources) {
+        psError(PS_ERR_UNEXPECTED_NULL, true,
+                "A list of sources is required for convolving to a target PSF.");
+        return false;
+    }
+    PS_ASSERT_INT_NONNEGATIVE(footprint, false);
+    // regionSize can be just about anything (except maybe negative, but it can be NAN)
+    PS_ASSERT_FLOAT_LARGER_THAN(stampSpacing, 0.0, false);
+    // Don't care what threshold is
+    if (sources) {
+        PS_ASSERT_ARRAY_NON_NULL(sources, false);
+    }
+    // stampsName may be anything
+    // targetWidth can be just about anything (except maybe negative, but it can be NAN)
+    // We'll check kernel type when we allocate the kernels
+    PS_ASSERT_INT_POSITIVE(size, false);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, false);
+    if (isisWidths || isisOrders) {
+        PS_ASSERT_VECTOR_NON_NULL(isisWidths, false);
+        PS_ASSERT_VECTOR_TYPE(isisWidths, PS_TYPE_F32, false);
+        PS_ASSERT_VECTOR_NON_NULL(isisOrders, false);
+        PS_ASSERT_VECTOR_TYPE(isisOrders, PS_TYPE_S32, false);
+        PS_ASSERT_VECTORS_SIZE_EQUAL(isisWidths, isisOrders, false);
+    }
+    PS_ASSERT_INT_NONNEGATIVE(inner, false);
+    PS_ASSERT_INT_NONNEGATIVE(ringsOrder, false);
+    PS_ASSERT_INT_POSITIVE(binning, false);
+    if (optimum) {
+        PS_ASSERT_VECTOR_NON_NULL(optFWHMs, false);
+        PS_ASSERT_INT_NONNEGATIVE(optOrder, false);
+        PS_ASSERT_FLOAT_LARGER_THAN(optThreshold, 0.0, false);
+        PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(optThreshold, 1.0, false);
+    }
+    PS_ASSERT_INT_POSITIVE(iter, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, false);
+    // Don't care about maskBad
+    // Don't care about maskBlank
+    if (isfinite(badFrac)) {
+        PS_ASSERT_FLOAT_LARGER_THAN(badFrac, 0.0, NULL);
+        PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(badFrac, 1.0, NULL);
+    }
+
+    // If the stamp footprint is smaller than the kernel size, then we won't get much signal in the outer
+    // parts of the kernel, which can result in bad matching artifacts.
+    if (footprint < size) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Stamp footprint (%d) should be larger than or equal to the kernel size (%d)",
+                footprint, size);
+        return false;
+    }
+
+    // Reset the output readout, just in case
+    if (convolved->image) {
+        psFree(convolved->image);
+        convolved->image = NULL;
+    }
+    if (convolved->mask) {
+        psFree(convolved->mask);
+        convolved->mask = NULL;
+    }
+    if (convolved->weight) {
+        psFree(convolved->weight);
+        convolved->weight = NULL;
+    }
+
+    psImage *inImage = NULL, *inMask = NULL; // Input image, mask, weight
+    if (input) {
+        inImage = input->image;
+        inMask = input->mask;
+    }
+
+    // Where does our weight map come from?
+    psImage *weight = NULL;             // Weight image to use
+    if (input && input->weight) {
+        weight = input->weight;
+    } else if (reference->weight) {
+        weight = reference->weight;
+    } else if (input) {
+        weight = input->image;
+    } else {
+        weight = reference->image;
+    }
+
+    // Putting important variable declarations here, since they are freed after a "goto" if there is an error.
+    psImage *subMask = NULL;            // Mask for subtraction
+    psRegion *region = NULL;            // Iso-kernel region
+    psString regionString = NULL;       // String for region
+    pmSubtractionStampList *stamps = NULL; // Stamps for matching PSF
+    psVector *solution = NULL;          // Solution to match PSF
+    pmSubtractionKernels *kernels = NULL; // Kernel basis functions
+
+    int numCols = reference->image->numCols, numRows = reference->image->numRows; // Image dimensions
+
+    memCheck("start");
+
+    subMask = pmSubtractionMask(reference->mask, inMask, maskBad, size, footprint, badFrac, useFFT);
+    if (!subMask) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate subtraction mask.");
+        return false;
+    }
+
+    memCheck("mask");
+
+    // Get region of interest
+    int xRegions = 1, yRegions = 1;     // Number of iso-kernel regions
+    float xRegionSize = 0, yRegionSize = 0; // Size of iso-kernel regions
+    if (isfinite(regionSize) && regionSize != 0.0) {
+        xRegions = numCols / regionSize + 1;
+        yRegions = numRows / regionSize + 1;
+        xRegionSize = (float)numCols / (float)xRegions;
+        yRegionSize = (float)numRows / (float)yRegions;
+        region = psRegionAlloc(NAN, NAN, NAN, NAN);
+    }
+
+    // Iterate over iso-kernel regions
+    for (int j = 0; j < yRegions; j++) {
+        for (int i = 0; i < xRegions; i++) {
+            psTrace("psModules.imcombine", 1, "Subtracting region %d of %d...\n",
+                    j * xRegions + i + 1, xRegions * yRegions);
+            if (region) {
+                *region = psRegionSet((int)(i * xRegionSize), (int)((i + 1) * xRegionSize),
+                                      (int)(j * yRegionSize), (int)((j + 1) * yRegionSize));
+                psFree(regionString);
+                regionString = psRegionToString(*region);
+                psTrace("psModules.imcombine", 3, "Iso-kernel region: %s out of %d,%d\n",
+                        regionString, numCols, numRows);
+            }
+
+            if (sources) {
+                stamps = pmSubtractionStampsSetFromSources(sources, subMask, region, stampSpacing,
+                                                           input ? 0 : 2 * footprint);
+            } else if (stampsName && strlen(stampsName) > 0) {
+                // Read stamps from file
+                psTrace("psModules.imcombine", 3, "Reading stamps from %s...\n", stampsName);
+                const char *stampFormat = input ? "%f %f" : "%f %f %f"; // Format for reading stamp file
+                psArray *stampsData = stampsData = psVectorsReadFromFile(stampsName, stampFormat);
+                psVector *xStamp = NULL, *yStamp = NULL, *fluxStamp = NULL; // Stamp positions and fluxes
+                if (!stampsData) {
+                    psError(PS_ERR_IO, false, "Unable to read stamps file %s", stampsName);
+                    goto ERROR;
+                }
+                xStamp = stampsData->data[0];
+                yStamp = stampsData->data[1];
+                if (!input) {
+                    fluxStamp = stampsData->data[2];
+                }
+
+                // Correct for IRAF/FITS (unit-offset) positions to C (zero-offset) positions
+                psBinaryOp(xStamp, xStamp, "-", psScalarAlloc(1.0, PS_TYPE_F32));
+                psBinaryOp(yStamp, yStamp, "-", psScalarAlloc(1.0, PS_TYPE_F32));
+
+                stamps = pmSubtractionStampsSet(xStamp, yStamp, fluxStamp, reference->image, subMask,
+                                                region, stampSpacing, input ? 0 : footprint + size);
+                psFree(stampsData);
+            }
+
+            // Define kernel basis functions
+            if (optimum && (type == PM_SUBTRACTION_KERNEL_ISIS || type == PM_SUBTRACTION_KERNEL_GUNK)) {
+                if (!getStamps(&stamps, reference, input, subMask, weight, NULL,
+                               threshold, stampSpacing, targetWidth, size, footprint)) {
+                    goto ERROR;
+                }
+                kernels = pmSubtractionKernelsOptimumISIS(type, size, inner, spatialOrder, optFWHMs, optOrder,
+                                                          stamps, footprint, optThreshold);
+                if (!kernels) {
+                    psErrorClear();
+                    psWarning("Unable to derive optimum ISIS kernel --- switching to default.");
+                }
+            }
+            if (kernels == NULL) {
+                // Not an ISIS/GUNK kernel, or the optimum kernel search failed
+                kernels = pmSubtractionKernelsGenerate(type, size, spatialOrder, isisWidths, isisOrders,
+                                                       inner, binning, ringsOrder);
+            }
+            psMetadataAddPtr(convolved->analysis, PS_LIST_TAIL, "SUBTRACTION.KERNEL",
+                             PS_DATA_UNKNOWN | PS_META_DUPLICATE_OK, "Subtraction kernels", kernels);
+
+            memCheck("kernels");
+
+            int numRejected = -1;               // Number of rejected stamps in each iteration
+            for (int k = 0; k < iter && numRejected != 0; k++) {
+                psLogMsg("psModules.imcombine", PS_LOG_INFO, "Iteration %d.", k);
+
+                if (!getStamps(&stamps, reference, input, subMask, weight, region,
+                               threshold, stampSpacing, targetWidth, size, footprint)) {
+                    goto ERROR;
+                }
+
+                psTrace("psModules.imcombine", 3, "Calculating equation...\n");
+                if (!pmSubtractionCalculateEquation(stamps, kernels, footprint)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to calculate least-squares equation.");
+                    goto ERROR;
+                }
+
+                memCheck("  calculate equation");
+
+                psTrace("psModules.imcombine", 3, "Solving equation...\n");
+                solution = pmSubtractionSolveEquation(solution, stamps);
+                if (!solution) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to calculate least-squares equation.");
+                    goto ERROR;
+                }
+
+                memCheck("  solve equation");
+
+                psTrace("psModules.imcombine", 3, "Rejecting stamps...\n");
+                numRejected = pmSubtractionRejectStamps(stamps, subMask, solution, footprint, rej, kernels);
+                if (numRejected < 0) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to reject stamps.");
+                    goto ERROR;
+                }
+                memCheck("  reject stamps");
+            }
+
+            if (numRejected > 0) {
+                psTrace("psModules.imcombine", 3, "Solving equation...\n");
+                solution = pmSubtractionSolveEquation(solution, stamps);
+                if (!solution) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to calculate least-squares equation.");
+                    goto ERROR;
+                }
+                (void)pmSubtractionRejectStamps(stamps, subMask, solution, footprint, NAN, kernels);
+            }
+            psFree(stamps);
+            stamps = NULL;
+
+            memCheck("solution");
+
+            {
+                psTrace("psModules.imcombine", 2, "Generating diagnostics...\n");
+                // Generate image with convolution kernels
+                int fullSize = 2 * size + 1 + 1;    // Full size of kernel
+                psImage *convKernels = psImageAlloc(5 * fullSize - 1, 5 * fullSize - 1, PS_TYPE_F32);
+                psImageInit(convKernels, NAN);
+                for (int j = -2; j <= 2; j++) {
+                    for (int i = -2; i <= 2; i++) {
+                        psImage *kernel = pmSubtractionKernelImage(solution, kernels, (float)i / 2.0,
+                                                                   (float)j / 2.0); // Image of the kernel
+                        if (!kernel) {
+                            psError(PS_ERR_UNKNOWN, false, "Unable to generate kernel image.");
+                            psFree(convKernels);
+                            goto ERROR;
+                        }
+
+                        if (psImageOverlaySection(convKernels, kernel, (i + 2) * fullSize,
+                                                  (j + 2) * fullSize, "=") == 0) {
+                            psError(PS_ERR_UNKNOWN, false, "Unable to overlay kernel image.");
+                            psFree(kernel);
+                            psFree(convKernels);
+                            goto ERROR;
+                        }
+                        psFree(kernel);
+                    }
+                }
+
+                psString comment = NULL; // Comment for metadata
+                psStringAppend(&comment, "Subtraction kernel for region %s", regionString);
+                psMetadataAddImage(convolved->analysis, PS_LIST_TAIL, "SUBTRACTION.KERNEL.IMAGE",
+                                   PS_META_DUPLICATE_OK, comment, convKernels);
+                psFree(comment);
+                psFree(convKernels);
+            }
+
+#ifdef TESTING
+            {
+                // Generate images of the kernel components
+                psMetadata *header = psMetadataAlloc(); // Header
+                for (int i = 0; i < solution->n; i++) {
+                    psString name = NULL;       // Header keyword
+                    psStringAppend(&name, "SOLN%04d", i);
+                    psMetadataAddF64(header, PS_LIST_TAIL, name, 0, NULL, solution->data.F64[i]);
+                    psFree(name);
+                }
+                psArray *kernelImages = pmSubtractionKernelSolutions(solution, kernels, 0.0, 0.0);
+                psFits *kernelFile = psFitsOpen("kernels.fits", "w");
+                (void)psFitsWriteImageCube(kernelFile, header, kernelImages, NULL);
+                psFitsClose(kernelFile);
+                psFree(kernelImages);
+                psFree(header);
+            }
+#endif
+
+            memCheck("diag outputs");
+
+            psTrace("psModules.imcombine", 2, "Convolving...\n");
+            if (!pmSubtractionConvolve(&convolved->image, &convolved->weight, &convolved->mask,
+                                       reference->image, reference->weight, subMask, maskBlank, region,
+                                       solution, kernels, useFFT)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to convolve reference image.");
+                goto ERROR;
+            }
+            psFree(kernels);
+            kernels = NULL;
+
+            // Put the solution on the metadata
+            {
+                psString comment = NULL; // Comment for metadata
+                psStringAppend(&comment, "Subtraction solution for region %s", regionString);
+                psMetadataAddVector(convolved->analysis, PS_LIST_TAIL, "SUBTRACTION.SOLUTION",
+                                    PS_META_DUPLICATE_OK, comment, solution);
+                psFree(comment);
+                if (region) {
+                    psMetadataAddPtr(convolved->analysis, PS_LIST_TAIL, "SUBTRACTION.REGION",
+                                     PS_META_DUPLICATE_OK | PS_DATA_REGION, comment, region);
+                } else {
+                    region = psRegionAlloc(0, numCols, 0, numRows);
+                    psMetadataAddPtr(convolved->analysis, PS_LIST_TAIL, "SUBTRACTION.REGION",
+                                     PS_META_DUPLICATE_OK | PS_DATA_REGION, comment, region);
+                    psFree(region);
+                    region = NULL;
+                }
+            }
+
+            psFree(solution);
+            solution = NULL;
+
+            // There is data in the readout now
+            convolved->data_exists = true;
+            if (convolved->parent) {
+                convolved->parent->data_exists = true;
+                convolved->parent->parent->data_exists = true;
+            }
+        }
+    }
+    psFree(region);
+    region = NULL;
+    psFree(regionString);
+    regionString = NULL;
+    psFree(subMask);
+    subMask = NULL;
+
+    if (!pmSubtractionBorder(convolved->image, convolved->weight, convolved->mask, size, maskBlank)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to set border of convolved image.");
+        goto ERROR;
+    }
+
+    memCheck("convolution");
+
+
+    return true;
+
+ERROR:
+    psFree(region);
+    psFree(regionString);
+    psFree(subMask);
+    psFree(kernels);
+    psFree(stamps);
+    psFree(solution);
+    return false;
+}
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionMatch.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionMatch.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionMatch.h	(revision 15358)
@@ -0,0 +1,43 @@
+#ifndef PM_SUBTRACTION_MATCH_H
+#define PM_SUBTRACTION_MATCH_H
+
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmSubtractionKernels.h"
+
+
+bool pmSubtractionMatch(pmReadout *convolved, ///< Output convolved data
+                        const pmReadout *reference, ///< Reference data
+                        const pmReadout *input, ///< Input data
+                        // Stamp parameters
+                        int footprint,  ///< Stamp half-size
+                        float regionSize, ///< Typical size of iso-kernel regions
+                        float stampSpacing, ///< Typical spacing between stamps
+                        float threshold, ///< Threshold for stamps
+                        const psArray *sources, ///< Sources for stamps
+                        const char *stampsName, ///< Filename for stamps
+                        float targetWidth, ///< Width of PSF for simulated target
+                        // Kernel parameters
+                        pmSubtractionKernelsType type, ///< Kernel type
+                        int size,       ///< Kernel half-size
+                        int order,      ///< Spatial polynomial order
+                        const psVector *widths, ///< ISIS Gaussian widths
+                        const psVector *orders, ///< ISIS Polynomial orders
+                        int inner,      ///< Inner radius for various kernel types
+                        int ringsOrder, ///< RINGS polynomial order
+                        int binning,    ///< SPAM kernel binning
+                        bool optimum,   ///< Search for optimum ISIS kernel?
+                        const psVector *optFWHMs, ///< FWHMs for optimum search
+                        int optOrder,   ///< Maximum order for optimum search
+                        float optThreshold, ///< Threshold for optimum search (0..1)
+                        // Operational parameters
+                        int iter,       ///< Rejection iterations
+                        float rej,      ///< Rejection threshold
+                        psMaskType maskBad, ///< Value to mask
+                        psMaskType maskBlank, ///< Mask for blank region
+                        float badFrac   ///< Maximum fraction of bad input pixels to accept
+    );
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionParams.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionParams.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionParams.c	(revision 15358)
@@ -0,0 +1,461 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <math.h>
+#include <pslib.h>
+
+#include "pmSubtractionStamps.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionParams.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#if 0
+// Convolve the reference stamp by the kernel
+static psKernel *convolveStamp(const pmSubtractionStamp *stamp, // Stamp to be convolved
+                               const psKernel *kernel, // Kernel by which to convolve
+                               int footprint // Size of area to be convolved
+    )
+{
+    psKernel *convolution = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Result
+    psKernel *reference = stamp->reference; // Reference stamp, to be convolved
+
+    // Range of kernel
+    int uMin = kernel->xMin;
+    int uMax = kernel->xMax;
+    int vMin = kernel->yMin;
+    int vMax = kernel->yMax;
+
+    for (int y = -footprint; y <= footprint; y++) {
+        psF32 *conv = &convolution->kernel[y][-footprint]; // Dereference convolution
+        for (int x = -footprint; x <= footprint; x++, conv++) {
+            *conv = 0.0;
+
+            int xStart = x + uMin;      // Start index for convolution
+            for (int v = vMin; v <= vMax; v++) {
+                psF32 *ref = &reference->kernel[y + v][xStart]; // Dereference reference image
+                psF32 *krnl = &kernel->kernel[v][uMin]; // Dereference kernel in x
+                for (int u = uMin; u <= uMax; u++, ref++, krnl++) {
+                    *conv += *ref * *krnl;
+                }
+            }
+        }
+    }
+
+    return convolution;
+}
+#endif
+
+// Accumulate cross-term sums for a stamp
+static void accumulateCross(double *sumI, // Sum of I(x)/sigma(x)^2
+                            double *sumII, // Sum of I(x)^2/sigma(x)^2
+                            double *sumIC, // Sum of I(x)conv(x)/sigma(x)^2
+                            const pmSubtractionStamp *stamp, // Stamp with weight
+                            const psKernel *input, // Input image, I(x)
+                            int kernelIndex, // Index for kernel component
+                            int footprint // Size of region of interest
+    )
+{
+    psKernel *weight = stamp->weight;   // Weight, sigma(x)^2
+    psKernel *convolution = stamp->convolutions->data[kernelIndex]; // Convolution of interest
+
+    for (int y = -footprint; y <= footprint; y++) {
+        psF32 *in = &input->kernel[y][-footprint]; // Dereference input
+        psF32 *wt = &weight->kernel[y][-footprint]; // Dereference weight
+        psF32 *conv = &convolution->kernel[y][-footprint]; // Dereference convolution
+        for (int x = -footprint; x <= footprint; x++, in++, wt++, conv++) {
+            double temp = *in / *wt; // Temporary product
+            *sumI += temp;
+            *sumII += *in * temp;
+            *sumIC += *conv * temp;
+        }
+    }
+    return;
+}
+
+// Accumulate convolution sums for a stamp
+static void accumulateConvolutions(double *sumC, // Sum of conv(x)/sigma(x)^2
+                                   double *sumCC, // Sum of conv(x)^2/sigma(x)^2
+                                   const pmSubtractionStamp *stamp, // Stamp with input and weight
+                                   int kernelIndex, // Index for kernel component
+                                   int footprint // Size of region of interest
+    )
+{
+    psKernel *weight = stamp->weight;   // Weight, sigma(x)^2
+    psKernel *convolution = stamp->convolutions->data[kernelIndex]; // Convolution of interest
+
+    for (int y = -footprint; y <= footprint; y++) {
+        psF32 *wt = &weight->kernel[y][-footprint]; // Dereference weight
+        psF32 *conv = &convolution->kernel[y][-footprint]; // Dereference convolution
+        for (int x = -footprint; x <= footprint; x++, wt++, conv++) {
+            double convNoise = *conv / *wt; // Temporary product
+            *sumC += convNoise;
+            *sumCC += *conv * convNoise;
+        }
+    }
+    return;
+}
+
+static double accumulateChi2(psKernel *input, // Input stamp
+                             pmSubtractionStamp *stamp, // Stamp with weight
+                             int kernelIndex, // Index for kernel component
+                             double coeff, // Coefficient of convolution
+                             double bg,  // Background term
+                             int footprint // Size of region of interest
+    )
+{
+    double chi2 = 0.0;
+    psKernel *weight = stamp->weight;   // Weight, sigma(x)^2
+    psKernel *convolution = stamp->convolutions->data[kernelIndex]; // Convolution of interest
+
+    for (int y = -footprint; y <= footprint; y++) {
+        psF32 *in = &input->kernel[y][-footprint]; // Dereference input
+        psF32 *wt = &weight->kernel[y][-footprint]; // Dereference weight
+        psF32 *conv = &convolution->kernel[y][-footprint]; // Dereference convolution
+        for (int x = -footprint; x <= footprint; x++, in++, wt++, conv++) {
+            chi2 += PS_SQR(*in - bg - coeff * *conv) / *wt;
+        }
+    }
+
+    return chi2;
+}
+
+// Return the initial value of chi^2
+static double initialChi2(psKernel *input, // Input stamp
+                          const pmSubtractionStamp *stamp, // Stamp with weight
+                          int footprint // Size of convolution
+    )
+{
+    psKernel *weight = stamp->weight;   // Weight map
+    psKernel *reference = stamp->reference; // Reference stamp
+
+    double chi2 = 0.0;                  // Chi^2
+    for (int y = -footprint; y <= footprint; y++) {
+        psF32 *in = &input->kernel[y][-footprint]; // Dereference input
+        psF32 *wt = &weight->kernel[y][-footprint]; // Dereference weight
+        psF32 *ref = &reference->kernel[y][-footprint]; // Derference reference
+        for (int x = -footprint; x <= footprint; x++, in++, wt++, ref++) {
+            float diff = *in - *ref;    // Temporary value
+            chi2 += PS_SQR(diff) / *wt;
+        }
+    }
+
+    return chi2;
+}
+
+// Subtract a convolution from the input
+static void subtractConvolution(psKernel *input, // Input stamp
+                                const pmSubtractionStamp *stamp, // Stamp with weight
+                                int kernelIndex, // Index for kernel component
+                                float coeff, // Coefficient of subtraction
+                                float bg, // Background term
+                                int footprint // Size of region of interest
+    )
+{
+    psKernel *convolution = stamp->convolutions->data[kernelIndex]; // Convolution of interest
+
+    for (int y = -footprint; y <= footprint; y++) {
+        psF32 *in = &input->kernel[y][-footprint]; // Dereference input
+        psF32 *conv = &convolution->kernel[y][-footprint]; // Dereference convolution
+        for (int x = -footprint; x <= footprint; x++, in++, conv++) {
+            *in -= *conv * coeff + bg;
+        }
+    }
+
+    return;
+}
+
+
+pmSubtractionKernels *pmSubtractionKernelsOptimumISIS(pmSubtractionKernelsType type, int size, int inner,
+                                                      int spatialOrder, const psVector *fwhms, int maxOrder,
+                                                      const pmSubtractionStampList *stamps, int footprint,
+                                                      float tolerance)
+{
+    if (type != PM_SUBTRACTION_KERNEL_ISIS && type != PM_SUBTRACTION_KERNEL_GUNK) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Invalid kernel type: %x\n", type);
+        return NULL;
+    }
+    PS_ASSERT_INT_NONNEGATIVE(size, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(inner, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(fwhms, NULL);
+    PS_ASSERT_VECTOR_TYPE(fwhms, PS_TYPE_F32, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(maxOrder, NULL);
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(footprint, NULL);
+    PS_ASSERT_FLOAT_LARGER_THAN(tolerance, 0.0, NULL);
+
+    // Generate the kernels to test
+    int numGaussians = fwhms->n;       // Number of Gaussians
+    int numKernels = numGaussians * (maxOrder + 1) * (maxOrder + 2) / 2; // Number of kernel components
+    psString params = NULL;             // Parameter, for description
+    for (int i = 0; i < numGaussians; i++) {
+        psStringAppend(&params, "%.2f,", fwhms->data.F32[i]);
+    }
+    params[strlen(params) - 1] = '\0';
+
+    psVector *orders = psVectorAlloc(numGaussians, PS_TYPE_S32); // Polynomial orders
+    psVectorInit(orders, maxOrder);
+    pmSubtractionKernels *kernels = p_pmSubtractionKernelsRawISIS(size, spatialOrder,
+                                                                  fwhms, orders); // Kernels
+    psFree(orders);
+    psFree(kernels->description);
+    kernels->description = NULL;
+    psStringAppend(&kernels->description, "OptISIS(%d,(%s),%d)", size, params, spatialOrder);
+    psFree(params);
+
+    // Need to save the stamp inputs --- we're changing the values!
+    int numStamps = stamps->num;        // Number of stamps
+    psArray *inputs = psArrayAlloc(numStamps); // Deep copies of the inputs
+    psVector *badStamps = psVectorAlloc(numStamps, PS_TYPE_U8); // Mark the bad stamps
+    psVectorInit(badStamps, 0);
+    for (int i = 0; i < numStamps; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (stamp->status != PM_SUBTRACTION_STAMP_CALCULATE && stamp->status != PM_SUBTRACTION_STAMP_USED) {
+            badStamps->data.U8[i] = 0xff;
+            continue;
+        }
+        psKernel *input = stamp->input; // Input image of interest
+        psImage *copy = psImageCopy(NULL, input->image, PS_TYPE_F32); // Copy of the image
+        inputs->data[i] = psKernelAllocFromImage(copy, size + footprint, size + footprint);
+        psFree(copy);                   // Drop reference
+    }
+
+    // Generate the convolutions, accumulate sums, and measure initial chi^2
+    double sum1 = 0.0;                  // sum of 1/sigma(x,y)^2
+    psVector *sumC = psVectorAlloc(numKernels, PS_TYPE_F64); // sum of R(x)*k(u)/sigma(x)^2
+    psVector *sumCC = psVectorAlloc(numKernels, PS_TYPE_F64); // sum of [R(x)*k(u)]^2/sigma(x)^2
+    psVectorInit(sumC, 0.0);
+    psVectorInit(sumCC, 0.0);
+    double lastChi2 = 0.0;              // Chi^2 from last iteration
+    int numPixels = 0;                  // Number of pixels contributing to chi^2
+    for (int i = 0; i < numStamps; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (badStamps->data.U8[i]) {
+            continue;
+        }
+        if (!pmSubtractionConvolveStamp(stamp, kernels, footprint)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to convolve stamp %d.", i);
+            psFree(inputs);
+            psFree(kernels);
+            psFree(badStamps);
+            return NULL;
+        }
+
+        // This sum is invariant to the kernel
+        psKernel *weight = stamp->weight; // Weight map for stamp
+        for (int v = -footprint; v <= footprint; v++) {
+            psF32 *wt = &weight->kernel[v][-footprint]; // Dereference weight map
+            for (int u = -footprint; u <= footprint; u++, wt++) {
+                sum1 += 1.0 / *wt;
+            }
+        }
+        if (!isfinite(sum1)) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                    "Sum of 1/sigma^2 is non-finite for stamp %d (%d,%d)\n",
+                    i, (int)stamp->x, (int)stamp->y);
+            psFree(inputs);
+            psFree(kernels);
+            psFree(badStamps);
+            return NULL;
+        }
+
+        for (int j = 0; j < numKernels; j++) {
+            accumulateConvolutions(&sumC->data.F64[j], &sumCC->data.F64[j], stamp, j, footprint);
+        }
+
+        lastChi2 += initialChi2(inputs->data[i], stamp, footprint);
+        numPixels += PS_SQR(2 * footprint + 1);
+    }
+    lastChi2 /= numPixels;
+
+    // Rank the kernel components
+    psVector *ranking = psVectorAlloc(numKernels, PS_TYPE_S32); // Ranking of the kernel components
+    psVectorInit(ranking, -1);
+    int cutIndex = -1;                  // Index at which to cut off kernels
+    for (int iter = 0; iter < numKernels; iter++) {
+        int bestIndex = -1;             // Index of best kernel component
+        double bestChi2 = INFINITY;     // Value of best chi^2
+        double bestCoeff = 0;           // Value of best coefficient
+        double bestBG = 0;              // Value of best background
+
+        for (int i = 0; i < numKernels; i++) {
+            if (ranking->data.S32[i] >= 0) {
+                continue;
+            }
+
+            double sumI = 0.0;          // sum of I(x)/sigma(x)^2
+            double sumII = 0.0;         // sum of I(x)^2/sigma(x)^2
+            double sumIC = 0.0;         // sum of I(x)C(x)/sigma(x)^2
+
+            for (int j = 0; j < numStamps; j++) {
+                if (badStamps->data.U8[j]) {
+                    continue;
+                }
+                pmSubtractionStamp *stamp = stamps->stamps->data[j]; // Stamp of interest
+                accumulateCross(&sumI, &sumII, &sumIC, stamp, inputs->data[j], i, footprint);
+            }
+
+            double invDet = 1.0 / (sum1 * sumCC->data.F64[i] - PS_SQR(sumC->data.F64[i])); // Determinant^-1
+            double coeff = invDet * (sum1 * sumIC - sumC->data.F64[i] * sumI); // Coefficient for kernel
+            double bg = invDet * (sumCC->data.F64[i] * sumI - sumC->data.F64[i] * sumIC); // Background
+
+            double chi2 = 0.0;          // Chi^2
+            for (int j = 0; j < numStamps; j++) {
+                if (badStamps->data.U8[j]) {
+                    continue;
+                }
+                pmSubtractionStamp *stamp = stamps->stamps->data[j]; // Stamp of interest
+                chi2 += accumulateChi2(inputs->data[j], stamp, i, coeff, bg, footprint);
+            }
+
+            if (chi2 < bestChi2) {
+                bestIndex = i;
+                bestCoeff = coeff;
+                bestChi2 = chi2;
+                bestBG = bg;
+            }
+
+            psTrace("psModules.imcombine", 8, "%d: %lf %lf %lf %lf %lf %lf\n", i, sum1, sumI, sumII, sumIC,
+                    sumC->data.F64[i], sumCC->data.F64[i]);
+            psTrace("psModules.imcombine", 6, "%d: %lf %lf %lf\n", i, coeff, bg, chi2);
+        }
+        bestChi2 /= numPixels;
+
+        if (bestIndex == -1) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to find best kernel component in round %d.", iter);
+            psFree(inputs);
+            psFree(sumC);
+            psFree(sumCC);
+            psFree(ranking);
+            psFree(kernels);
+            psFree(badStamps);
+            return NULL;
+        }
+
+        // And the winner is....
+        ranking->data.S32[bestIndex] = iter;
+        // Remove its contribution, and don't include it in the future.
+        for (int j = 0; j < numStamps; j++) {
+            if (badStamps->data.U8[j]) {
+                continue;
+            }
+            pmSubtractionStamp *stamp = stamps->stamps->data[j]; // Stamp of interest
+            subtractConvolution(inputs->data[j], stamp, bestIndex, bestCoeff, bestBG, footprint);
+        }
+
+        double diff = lastChi2 - bestChi2; // Difference in chi^2 between iterations
+
+        psTrace("psModules.imcombine", 3, "The winner of round %d is %d (%f,%d,%d): %lf (%lf) --> %lf\n",
+                iter, bestIndex, kernels->widths->data.F32[bestIndex], kernels->u->data.S32[bestIndex],
+                kernels->v->data.S32[bestIndex], bestCoeff, diff, bestChi2);
+
+        if (fabsf(diff) < tolerance) {
+            cutIndex = iter;
+            break;
+        }
+
+        lastChi2 = bestChi2;
+    }
+    psFree(inputs);
+    psFree(sumC);
+    psFree(sumCC);
+
+    if (cutIndex < 0) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to converge to tolerance %g\n", tolerance);
+        psFree(ranking);
+        psFree(kernels);
+        psFree(badStamps);
+        return NULL;
+    }
+
+    int newSize = cutIndex + 1;         // Size of new kernel basis set
+    psTrace("psModules.imcombine", 2, "Accepting %d kernels.\n", newSize);
+    psVector *uNew = psVectorAlloc(newSize, PS_TYPE_S32);
+    psVector *vNew = psVectorAlloc(newSize, PS_TYPE_S32);
+    psVector *widthsNew = psVectorAlloc(newSize, PS_TYPE_F32);
+    psArray *preCalcNew = psArrayAlloc(newSize);
+    psArray *convNew = psArrayAlloc(numStamps);
+    for (int i = 0; i < numStamps; i++) {
+        if (badStamps->data.U8[i]) {
+            continue;
+        }
+        convNew->data[i] = psArrayAlloc(newSize);
+    }
+
+    for (int i = 0; i < numKernels; i++) {
+        int rank = ranking->data.S32[i]; // This kernel component's ranking
+        if (rank >= 0 && rank < newSize) {
+            uNew->data.S32[rank] = kernels->u->data.S32[i];
+            vNew->data.S32[rank] = kernels->v->data.S32[i];
+            widthsNew->data.F32[rank] = kernels->widths->data.F32[i];
+            preCalcNew->data[rank] = psMemIncrRefCounter(kernels->preCalc->data[i]);
+
+            for (int j = 0; j < numStamps; j++) {
+                if (badStamps->data.U8[j]) {
+                    continue;
+                }
+                pmSubtractionStamp *stamp = stamps->stamps->data[j]; // Stamp of interest
+                psArray *convolutions = convNew->data[j]; // Convolutions for this stamp
+                convolutions->data[rank] = psMemIncrRefCounter(stamp->convolutions->data[i]);
+            }
+        }
+    }
+    psFree(kernels->u);
+    psFree(kernels->v);
+    psFree(kernels->widths);
+    psFree(kernels->preCalc);
+    kernels->u = uNew;
+    kernels->v = vNew;
+    kernels->widths = widthsNew;
+    kernels->preCalc = preCalcNew;
+    kernels->num = newSize;
+
+    for (int i = 0; i < numStamps; i++) {
+        if (badStamps->data.U8[i]) {
+            continue;
+        }
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        psFree(stamp->convolutions);
+        stamp->convolutions = convNew->data[i];
+    }
+
+    psFree(badStamps);
+    psFree(ranking);
+
+    // Maintain photometric scaling
+    if (type == PM_SUBTRACTION_KERNEL_ISIS) {
+        psKernel *subtract = kernels->preCalc->data[0]; // Kernel to subtract from the rest
+        for (int i = 1; i < newSize; i++) {
+            if (kernels->u->data.S32[i] % 2 == 0 && kernels->v->data.S32[i] % 2 == 0) {
+                psKernel *kernel = kernels->preCalc->data[i]; // Kernel of interest
+                psBinaryOp(kernel->image, kernel->image, "-", subtract->image);
+            }
+        }
+    } else if (type == PM_SUBTRACTION_KERNEL_GUNK) {
+        psStringPrepend(&kernels->description, "GUNK=");
+        psStringAppend(&kernels->description, "+POIS(%d,%d)", inner, spatialOrder);
+
+        for (int i = 0; i < newSize; i++) {
+            if (kernels->u->data.S32[i] % 2 == 0 && kernels->v->data.S32[i] % 2 == 0) {
+                psKernel *kernel = kernels->preCalc->data[i]; // Kernel of interest
+                kernel->kernel[0][0] -= 1.0;
+            }
+        }
+
+        if (!p_pmSubtractionKernelsAddGrid(kernels, numGaussians, inner)) {
+            psAbort("Should never get here.");
+        }
+
+        kernels->type = PM_SUBTRACTION_KERNEL_GUNK;
+
+        kernels->subIndex = PS_SQR(2 * inner + 1) / 2 + numGaussians;
+        assert(kernels->u->data.S32[kernels->subIndex] == 0 &&
+               kernels->v->data.S32[kernels->subIndex] == 0);
+    }
+
+    return kernels;
+}
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionParams.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionParams.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionParams.h	(revision 15358)
@@ -0,0 +1,20 @@
+#ifndef PM_SUBTRACTION_PARAMS_H
+#define PM_SUBTRACTION_PARAMS_H
+
+#include <pslib.h>
+#include <pmSubtractionKernels.h>
+#include <pmSubtractionStamps.h>
+
+/// Generate a set of optimum kernels for ISIS (or GUNK)
+pmSubtractionKernels *pmSubtractionKernelsOptimumISIS(pmSubtractionKernelsType type, ///< Kernel type
+                                                      int size, ///< Half-size of kernel
+                                                      int inner, ///< Inner radius for GUNK
+                                                      int spatialOrder, ///< Spatial polynomial order
+                                                      const psVector *fwhms, ///< Gaussian FWHMs to try
+                                                      int maxOrder, ///< Maximum polynomial order
+                                                      const pmSubtractionStampList *stamps, ///< Stamps
+                                                      int footprint, ///< Convolution footprint for stamps
+                                                      float tolerance ///< Maximum difference in chi^2
+    );
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionStamps.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionStamps.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionStamps.c	(revision 15358)
@@ -0,0 +1,573 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+// All these includes required to get stamps out of an array of pmSources
+#include "pmMoments.h"
+#include "pmPeaks.h"
+#include "pmResiduals.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+
+
+#include "pmSubtraction.h"
+#include "pmSubtractionStamps.h"
+
+#define STAMP_LIST_BUFFER 20            // Number of stamps to add to list at a time
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Private (file-static) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Free function for pmSubtractionStampList
+static void subtractionStampListFree(pmSubtractionStampList *list // Stamp list to free
+                                     )
+{
+    psFree(list->stamps);
+    psFree(list->regions);
+    psFree(list->x);
+    psFree(list->y);
+    psFree(list->flux);
+}
+
+// Free function for pmSubtractionStamp
+static void subtractionStampFree(pmSubtractionStamp *stamp // Stamp to free
+                                 )
+{
+    psFree(stamp->matrix);
+    psFree(stamp->vector);
+    psFree(stamp->reference);
+    psFree(stamp->input);
+    psFree(stamp->weight);
+    psFree(stamp->convolutions);
+}
+
+// Is this region OK?
+static bool checkStampRegion(int x, int y, // Coordinates of stamp
+                             const psRegion *region // Region of interest
+                             )
+{
+    if (!region) {
+        return true;
+    }
+    return (x < region->x0 || x > region->x1 || y < region->y0 || y > region->y1) ?
+        false : true;
+}
+
+// Is this position unmasked?
+static bool checkStampMask(int x, int y, // Coordinates of stamp
+                           const psImage *mask
+                           )
+{
+    if (!mask) {
+        return true;
+    }
+    if (x < 0 || x >= mask->numCols || y < 0 || y >= mask->numRows) {
+        return false;
+    }
+    return (mask->data.PS_TYPE_MASK_DATA[y][x] & (PM_SUBTRACTION_MASK_BORDER |
+                                                  PM_SUBTRACTION_MASK_FOOTPRINT | PM_SUBTRACTION_MASK_REJ)) ?
+        false : true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+pmSubtractionStampList *pmSubtractionStampListAlloc(int numCols, int numRows, const psRegion *region,
+                                                    float spacing)
+{
+    pmSubtractionStampList *list = psAlloc(sizeof(pmSubtractionStampList)); // Stamp list to return
+    psMemSetDeallocator(list, (psFreeFunc)subtractionStampListFree);
+
+    // Get region in which to find stamps: [xMin:xMax,yMin:yMax]
+    int xMin = 0, xMax = numCols, yMin = 0, yMax = numRows;
+    if (region) {
+        xMin = PS_MAX(region->x0, xMin);
+        xMax = PS_MIN(region->x1, xMax);
+        yMin = PS_MAX(region->y0, yMin);
+        yMax = PS_MIN(region->y1, yMax);
+    }
+    int xSize = xMax - xMin, ySize = yMax - yMin; // Size of region of interest
+    int xStamps = xSize / spacing + 1, yStamps = ySize / spacing + 1; // Number of stamps in x and y
+
+    list->num = xStamps * yStamps;
+    list->stamps = psArrayAlloc(list->num);
+    list->regions = psArrayAlloc(list->num);
+
+    for (int y = 0, index = 0; y < yStamps; y++) {
+        int yStart = yMin + y * ySize / (yStamps + 1); // Subregion starts here
+        int yStop = yMin + (y + 1) * ySize / (yStamps + 1) - 1; // Subregion stops here
+        assert(yStart >= yMin && yStop < yMax);
+
+        for (int x = 0; x < xStamps; x++, index++) {
+            int xStart = xMin + x * xSize / (xStamps + 1); // Subregion starts here
+            int xStop = xMin + (x + 1) * xSize / (xStamps + 1) - 1; // Subregion stops here
+            assert(xStart >= xMin && xStop < xMax);
+
+            list->stamps->data[index] = pmSubtractionStampAlloc();
+            list->regions->data[index] = psRegionAlloc(xStart, xStop, yStart, yStop);
+        }
+    }
+
+    list->x = NULL;
+    list->y = NULL;
+    list->flux = NULL;
+
+    return list;
+}
+
+pmSubtractionStamp *pmSubtractionStampAlloc(void)
+{
+    pmSubtractionStamp *stamp = psAlloc(sizeof(pmSubtractionStamp)); // Stamp to return
+    psMemSetDeallocator(stamp, (psFreeFunc)subtractionStampFree);
+
+    stamp->x = NAN;
+    stamp->y = NAN;
+    stamp->flux = NAN;
+    stamp->xNorm = NAN;
+    stamp->yNorm = NAN;
+    stamp->matrix = NULL;
+    stamp->vector = NULL;
+    stamp->status = PM_SUBTRACTION_STAMP_INIT;
+
+    stamp->reference = NULL;
+    stamp->input = NULL;
+    stamp->weight = NULL;
+    stamp->convolutions = NULL;
+
+    return stamp;
+}
+
+
+pmSubtractionStampList *pmSubtractionStampsFind(pmSubtractionStampList *stamps, const psImage *image,
+                                                const psImage *subMask, const psRegion *region,
+                                                float threshold, float spacing)
+{
+    PS_ASSERT_IMAGE_NON_NULL(image, NULL);
+    PS_ASSERT_IMAGE_TYPE(image, PS_TYPE_F32, NULL);
+    if (subMask) {
+        PS_ASSERT_IMAGE_NON_NULL(subMask, NULL);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(image, subMask, NULL);
+        PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_MASK, NULL);
+    }
+    PS_ASSERT_FLOAT_LARGER_THAN(spacing, 0.0, NULL);
+    if (region) {
+        if (psRegionIsNaN(*region)) {
+            psString string = psRegionToString(*region);
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Input region (%s) contains NAN values", string);
+            psFree(string);
+            return false;
+        }
+        if (region->x0 < 0 || region->x1 > image->numCols ||
+            region->y0 < 0 || region->y1 > image->numRows) {
+            psString string = psRegionToString(*region);
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Input region (%s) does not fit in image (%dx%d)",
+                    string, image->numCols, image->numRows);
+            psFree(string);
+            return false;
+        }
+    }
+
+    int numRows = image->numRows, numCols = image->numCols; // Size of image
+
+    if (!stamps) {
+        stamps = pmSubtractionStampListAlloc(numCols, numRows, region, spacing);
+    }
+
+    int numStamps = stamps->num;        // Number of stamp regions
+    int numFound = 0;                   // Number of stamps found
+    int numValid = 0;                   // Number of valid regions
+    for (int i = 0; i < numStamps; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+
+        // Only find a new stamp if we need to
+        if (stamp->status != PM_SUBTRACTION_STAMP_REJECTED && stamp->status != PM_SUBTRACTION_STAMP_INIT) {
+            continue;
+        }
+        numValid++;
+
+        float xStamp = 0, yStamp = 0;   // Coordinates of stamp
+        float fluxStamp = NAN;          // Flux of stamp
+        bool goodStamp = false;         // Found a good stamp?
+
+        // A couple different ways of finding stamps:
+        if (stamps->x && stamps->y) {
+            // Get the next stamp from the list
+            psVector *xList = stamps->x->data[i], *yList = stamps->y->data[i]; // Coordinate lists
+            psVector *fluxList = stamps->flux->data[i]; // List of stamp fluxes
+
+            // Take stamp off the top of the (sorted) list
+            if (xList->n > 0) {
+                int index = xList->n - 1; // Index of new stamp
+                xStamp = xList->data.F32[index];
+                yStamp = yList->data.F32[index];
+                fluxStamp = fluxList->data.F32[index];
+
+                // Chop off the top of the list
+                xList->n = index;
+                yList->n = index;
+                fluxList->n = index;
+
+                goodStamp = true;
+            }
+        } else {
+            // Use a simple method of automatically finding stamps --- take the highest pixel in the subregion
+            fluxStamp = threshold;
+            psRegion *subRegion = stamps->regions->data[i]; // Sub-region of interest
+            for (int y = subRegion->y0; y <= subRegion->y1 ; y++) {
+                for (int x = subRegion->x0; x <= subRegion->y1 ; x++) {
+                    if (checkStampMask(x, y, subMask) && image->data.F32[y][x] > fluxStamp) {
+                        fluxStamp = image->data.F32[y][x];
+                        xStamp = x;
+                        yStamp = y;
+                        goodStamp = true;
+                    }
+                }
+            }
+        }
+
+        if (goodStamp) {
+            stamp->x = xStamp;
+            stamp->y = yStamp;
+            stamp->flux = fluxStamp;
+
+            // Reset the postage stamps since we're making a new stamp
+            psFree(stamp->reference);
+            psFree(stamp->input);
+            psFree(stamp->weight);
+            stamp->reference = stamp->input = stamp->weight = NULL;
+
+            stamp->status = PM_SUBTRACTION_STAMP_CALCULATE;
+            numFound++;
+            psTrace("psModules.imcombine", 5, "Found stamp in subregion %d: %d,%d\n",
+                    i, (int)stamp->x, (int)stamp->y);
+        } else {
+            stamp->status = PM_SUBTRACTION_STAMP_NONE;
+        }
+    }
+
+    if (numValid > 0) {
+        psLogMsg("psModules.imcombine", PS_LOG_INFO, "Found %d stamps", numFound);
+    }
+
+    return stamps;
+}
+
+
+pmSubtractionStampList *pmSubtractionStampsSet(const psVector *x, const psVector *y, const psVector *flux,
+                                               const psImage *image, const psImage *subMask,
+                                               const psRegion *region, float spacing, int exclusionZone)
+{
+    PS_ASSERT_VECTOR_NON_NULL(x, NULL);
+    PS_ASSERT_VECTOR_TYPE(x, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(y, NULL);
+    PS_ASSERT_VECTOR_TYPE(y, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(y, x, NULL);
+    if (flux) {
+        PS_ASSERT_VECTOR_NON_NULL(flux, NULL);
+        PS_ASSERT_VECTOR_TYPE(flux, PS_TYPE_F32, NULL);
+        PS_ASSERT_VECTORS_SIZE_EQUAL(flux, x, NULL);
+    } else {
+        PS_ASSERT_IMAGE_NON_NULL(image, NULL);
+    }
+    if (subMask) {
+        PS_ASSERT_IMAGE_NON_NULL(subMask, NULL);
+        PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_MASK, NULL);
+        if (image) {
+            PS_ASSERT_IMAGE_NON_NULL(image, NULL);
+            PS_ASSERT_IMAGES_SIZE_EQUAL(image, subMask, NULL);
+        }
+    }
+    PS_ASSERT_FLOAT_LARGER_THAN(spacing, 0.0, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(exclusionZone, NULL);
+
+    int numStars = x->n;                // Number of stars
+    psVector *exclude = psVectorAlloc(numStars, PS_TYPE_U8); // Exclude a star?
+    psVectorInit(exclude, 0);
+
+    // Apply exclusion zone around each star --- important when we're convolving to a specified PSF; less so
+    // when we're trying to get a good subtraction.
+    if (exclusionZone > 0) {
+        psTrace("psModules.imcombine", 2, "Applying exclusion zone of %d pixels for stamps\n", exclusionZone);
+        // We use something resembling a binary search --- coordinates are sorted in the x dimension, and then
+        // to exclude stars within a nominated distance, we need only examine (i.e., calculate the
+        // 2-dimensional distance to) other stars in the list that are within that distance of the x
+        // coordinate of the star of interest.  By marking both stars when we find a violation of the
+        // exclusion zone, we need only search upwards from the x coordinate of the star of interest.
+
+        int minDistance2 = PS_SQR(exclusionZone); // Minimum square distance for other stars
+        psVector *indexes = psVectorSortIndex(NULL, x); // Indices for sorting in x
+        for (int i = 0; i < numStars - 1; i++) {
+            int iIndex = indexes->data.S32[i]; // Index for star i
+            if (exclude->data.U8[iIndex]) {
+                continue;
+            }
+            float ix = x->data.F32[iIndex], iy = y->data.F32[iIndex]; // Coordinates for star i
+            float jx, jy;                   // Coordinates for star j
+            for (int j = i + 1, jIndex = indexes->data.S32[j];
+                 j < numStars && (jx = x->data.F32[jIndex]) < ix + exclusionZone;
+                 j++, jIndex = indexes->data.S32[j]) {
+                jy = y->data.F32[jIndex];
+                if (PS_SQR(ix - jx) + PS_SQR(iy - jy) < minDistance2) {
+                    exclude->data.U8[iIndex] = 0xff;
+                    exclude->data.U8[jIndex] = 0xff;
+                    psTrace("psModules.imcombine", 5, "Excluding stamps %d,%d and %d,%d\n",
+                            (int)x->data.F32[iIndex], (int)y->data.F32[iIndex],
+                            (int)x->data.F32[jIndex], (int)y->data.F32[jIndex]);
+                    // Would 'break' here, but there might be more stamps within the exclusion zone.
+                }
+            }
+        }
+        psFree(indexes);
+    }
+
+    pmSubtractionStampList *stamps = pmSubtractionStampListAlloc(subMask->numCols, subMask->numRows,
+                                                                 region, spacing); // Stamp list
+    int numStamps = stamps->num;        // Number of stamps
+
+    // Initialise the lists
+    stamps->x = psArrayAlloc(numStamps);
+    stamps->y = psArrayAlloc(numStamps);
+    stamps->flux = psArrayAlloc(numStamps);
+    for (int i = 0; i < numStamps; i++) {
+        stamps->x->data[i] = psVectorAllocEmpty(STAMP_LIST_BUFFER, PS_TYPE_F32);
+        stamps->y->data[i] = psVectorAllocEmpty(STAMP_LIST_BUFFER, PS_TYPE_F32);
+        stamps->flux->data[i] = psVectorAllocEmpty(STAMP_LIST_BUFFER, PS_TYPE_F32);
+    }
+
+    // Put the stars into their appropriate subregions
+    for (int i = 0; i < numStars; i++) {
+        if (exclude->data.U8[i]) {
+            // Star has been excluded
+            continue;
+        }
+        float xStamp = x->data.F32[i], yStamp = y->data.F32[i]; // Coordinates of stamp
+        int xPix = xStamp + 0.5, yPix = yStamp + 0.5; // Pixel coordinate of stamp
+        if (!checkStampRegion(xPix, yPix, region)) {
+            // It's not in the big region
+            continue;
+        }
+        if (!checkStampMask(xPix, yPix, subMask)) {
+            // Not a good stamp
+            continue;
+        }
+
+        for (int j = 0; j < numStamps; j++) {
+            psRegion *subRegion = stamps->regions->data[j]; // Subregion of interest
+            if (checkStampRegion(xPix, yPix, subRegion)) {
+                psVector *xList = stamps->x->data[j], *yList = stamps->y->data[j]; // Pixel lists
+                psVector *fluxList = stamps->flux->data[j]; // Flux list
+
+                int index = xList->n;   // Index of new stamp candidate
+                xList->data.F32[index] = xStamp;
+                yList->data.F32[index] = yStamp;
+
+                if (flux) {
+                    fluxList->data.F32[index] = flux->data.F32[i];
+                } else {
+                    fluxList->data.F32[index] = image->data.F32[yPix][xPix];
+                }
+
+                psVectorExtend(xList, STAMP_LIST_BUFFER, 1);
+                psVectorExtend(yList, STAMP_LIST_BUFFER, 1);
+                psVectorExtend(fluxList, STAMP_LIST_BUFFER, 1);
+
+                break;
+            }
+        }
+    }
+    psFree(exclude);
+
+    // Sort the list by flux, with the brightest last
+    for (int i = 0; i < numStamps; i++) {
+        psVector *xList = stamps->x->data[i], *yList = stamps->y->data[i]; // Pixel lists
+        psVector *fluxList = stamps->flux->data[i]; // Flux list
+
+        psVector *indexes = psVectorSortIndex(NULL, fluxList); // Indices to sort flux
+        int num = indexes->n;           // Number of candidate stamps in this subregion
+
+        psVector *xSorted = psVectorAlloc(num, PS_TYPE_F32); // Sorted version of x list
+        psVector *ySorted = psVectorAlloc(num, PS_TYPE_F32); // Sorted version of y list
+        psVector *fluxSorted = psVectorAlloc(num, PS_TYPE_F32); // Sorted version of flux list
+        for (int j = 0; j < num; j++) {
+            int k = indexes->data.S32[j]; // Sorted index
+            xSorted->data.F32[j] = xList->data.F32[k];
+            ySorted->data.F32[j] = yList->data.F32[k];
+            fluxSorted->data.F32[j] = fluxList->data.F32[k];
+        }
+        psFree(indexes);
+
+        psFree(stamps->x->data[i]);
+        psFree(stamps->y->data[i]);
+        psFree(stamps->flux->data[i]);
+
+        stamps->x->data[i] = xSorted;
+        stamps->y->data[i] = ySorted;
+        stamps->flux->data[i] = fluxSorted;
+    }
+
+
+    return stamps;
+}
+
+
+bool pmSubtractionStampsExtract(pmSubtractionStampList *stamps, psImage *reference, psImage *input,
+                                psImage *weight, int footprint, int kernelSize)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PS_ASSERT_IMAGE_NON_NULL(reference, false);
+    PS_ASSERT_IMAGE_TYPE(reference, PS_TYPE_F32, false);
+    if (input) {
+        PS_ASSERT_IMAGE_NON_NULL(input, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(input, reference, false);
+        PS_ASSERT_IMAGE_TYPE(input, PS_TYPE_F32, false);
+    }
+    if (weight) {
+        PS_ASSERT_IMAGE_NON_NULL(weight, false);
+        PS_ASSERT_IMAGES_SIZE_EQUAL(weight, reference, false);
+        PS_ASSERT_IMAGE_TYPE(weight, PS_TYPE_F32, false);
+    }
+    PS_ASSERT_INT_NONNEGATIVE(footprint, false);
+    PS_ASSERT_INT_NONNEGATIVE(kernelSize, false);
+
+    int numCols = reference->numCols, numRows = reference->numRows; // Size of images
+    int size = kernelSize + footprint; // Size of postage stamps
+
+    if (!weight) {
+        // Use the input (or if I must, the reference) as a rough approximation to the variance map, and HOPE
+        // that it's positive.
+        weight = input ? input : reference;
+    }
+
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (!stamp || stamp->status != PM_SUBTRACTION_STAMP_CALCULATE) {
+            continue;
+        }
+
+        if (isnan(stamp->xNorm)) {
+            stamp->xNorm = 2.0 * (stamp->x - (float)numCols/2.0) / (float)numCols;
+        }
+        if (isnan(stamp->yNorm)) {
+            stamp->yNorm = 2.0 * (stamp->y - (float)numRows/2.0) / (float)numRows;
+        }
+
+        int x = stamp->x + 0.5, y = stamp->y + 0.5; // Stamp coordinates
+        if (x < size || x > numCols - size || y < size || y > numRows - size) {
+            psError(PS_ERR_UNKNOWN, false, "Stamp %d (%d,%d) is within the image border.\n", i, x, y);
+            return false;
+        }
+
+        psRegion region = psRegionSet(x - size, x + size + 1, y - size, y + size + 1); // Region of interest
+
+        psImage *refSub = psImageSubset(reference, region); // Subimage with stamp
+        stamp->reference = psKernelAllocFromImage(refSub, size, size);
+        psFree(refSub);                 // Drop reference
+
+        if (input) {
+            psImage *inSub = psImageSubset(input, region); // Subimage with stamp
+            stamp->input = psKernelAllocFromImage(inSub, size, size);
+            psFree(inSub);              // Drop reference
+        }
+
+        psImage *wtSub = psImageSubset(weight, region); // Subimage with stamp
+        stamp->weight = psKernelAllocFromImage(wtSub, size, size);
+        psFree(wtSub);                 // Drop reference
+    }
+
+    return true;
+}
+
+
+bool pmSubtractionStampsGenerate(pmSubtractionStampList *stamps, float fwhm, int footprint, int kernelSize)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(fwhm, 0.0, false);
+    PS_ASSERT_INT_NONNEGATIVE(footprint, false);
+    PS_ASSERT_INT_NONNEGATIVE(kernelSize, false);
+
+    int size = kernelSize + footprint; // Size of postage stamps
+    int num = stamps->num;              // Number of stamps
+    float sigma = fwhm / (2.0 * sqrtf(2.0 * log(2.0))); // Gaussian sigma
+
+    for (int i = 0; i < num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (!(stamp->status & PM_SUBTRACTION_STAMP_CALCULATE)) {
+            continue;
+        }
+
+        float x = stamp->x, y = stamp->y; // Coordinates of stamp
+        float flux = stamp->flux; // Flux of star
+        if (!isfinite(flux)) {
+            psWarning("Unable to generate PSF for stamp %d --- bad flux.", i);
+            stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
+            continue;
+        }
+
+        float xStamp = x - (int)(x + 0.5); // x coordinate of star in stamp frame
+        float yStamp = y - (int)(y + 0.5); // y coordinate of star in stamp frame
+
+        psFree(stamp->input);
+        stamp->input = psKernelAlloc(-size, size, -size, size);
+        psKernel *input = stamp->input; // Target stamp
+
+        // Put in a Waussian, just for fun!
+        for (int v = -size; v <= size; v++) {
+            for (int u = -size; u <= size; u++) {
+                float z = (PS_SQR(u + xStamp) + PS_SQR(v + yStamp)) / (2.0 * PS_SQR(sigma));
+                input->kernel[v][u] = flux / sigma * 0.5 * M_2_SQRTPI * M_SQRT1_2 / (1.0 + z + PS_SQR(z));
+            }
+        }
+
+    }
+
+    return true;
+}
+
+
+pmSubtractionStampList *pmSubtractionStampsSetFromSources(const psArray *sources, const psImage *subMask,
+                                                          const psRegion *region, float spacing,
+                                                          int exclusionZone)
+{
+    PS_ASSERT_ARRAY_NON_NULL(sources, NULL);
+    // Let pmSubtractionStampsSet take care of the rest of the assertions
+
+    int numSources = sources->n;          // Number of stars
+
+    psVector *x = psVectorAlloc(numSources, PS_TYPE_F32); // x coordinates
+    psVector *y = psVectorAlloc(numSources, PS_TYPE_F32); // y coordinates
+    psVector *flux = psVectorAlloc(numSources, PS_TYPE_F32); // Fluxes
+
+    for (int i = 0; i < numSources; i++) {
+        pmSource *source = sources->data[i]; // Source of interest
+        if (source->modelPSF) {
+            x->data.F32[i] = source->modelPSF->params->data.F32[PM_PAR_XPOS];
+            y->data.F32[i] = source->modelPSF->params->data.F32[PM_PAR_YPOS];
+        } else {
+            x->data.F32[i] = source->peak->xf;
+            y->data.F32[i] = source->peak->yf;
+        }
+        flux->data.F32[i] = powf(10.0, -0.4 * source->psfMag);
+    }
+
+    pmSubtractionStampList *stamps = pmSubtractionStampsSet(x, y, flux, NULL, subMask, region,
+                                                            spacing, exclusionZone); // Stamps to return
+    psFree(x);
+    psFree(y);
+    psFree(flux);
+
+    if (!stamps) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to set stamps from sources.");
+    }
+
+    return stamps;
+}
Index: /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionStamps.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionStamps.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/imcombine/pmSubtractionStamps.h	(revision 15358)
@@ -0,0 +1,116 @@
+#ifndef PM_SUBTRACTION_STAMPS_H
+#define PM_SUBTRACTION_STAMPS_H
+
+#include <pslib.h>
+
+#include "pmSubtractionKernels.h"
+
+/// Status of stamp
+typedef enum {
+    PM_SUBTRACTION_STAMP_INIT,          ///< Initial state
+    PM_SUBTRACTION_STAMP_USED,          ///< Use this stamp
+    PM_SUBTRACTION_STAMP_REJECTED,      ///< This stamp has been rejected
+    PM_SUBTRACTION_STAMP_CALCULATE,     ///< Calculate matrix and vector values for this stamp
+    PM_SUBTRACTION_STAMP_NONE           ///< No stamp in this region
+} pmSubtractionStampStatus;
+
+/// A list of stamps
+typedef struct {
+    long num;                           ///< Number of stamps
+    psArray *stamps;                    ///< The stamps
+    psArray *regions;                   ///< Regions for each stamp
+    psArray *x, *y;                     ///< Coordinates for possible stamps (or NULL)
+    psArray *flux;                      ///< Fluxes for possible stamps (or NULL)
+} pmSubtractionStampList;
+
+/// Allocate a list of stamps
+pmSubtractionStampList *pmSubtractionStampListAlloc(int numCols, // Number of columns in image
+                                                    int numRows, // Number of rows in image
+                                                    const psRegion *region, // Region for stamps, or NULL
+                                                    float spacing // Rough average spacing between stamps
+    );
+
+/// Assertion for stamp list to be valid
+#define PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(LIST, RETURNVALUE) { \
+    PS_ASSERT_PTR_NON_NULL(LIST, RETURNVALUE); \
+    PS_ASSERT_ARRAY_NON_NULL((LIST)->stamps, RETURNVALUE); \
+    PS_ASSERT_ARRAY_NON_NULL((LIST)->regions, RETURNVALUE); \
+    PS_ASSERT_INT_POSITIVE((LIST)->num, RETURNVALUE); \
+    PS_ASSERT_ARRAY_SIZE((LIST)->stamps, (LIST)->num, RETURNVALUE); \
+    PS_ASSERT_ARRAY_SIZE((LIST)->regions, (LIST)->num, RETURNVALUE); \
+    if ((LIST)->x || (LIST)->y || (LIST)->flux) { \
+        PS_ASSERT_ARRAY_NON_NULL((LIST)->x, RETURNVALUE); \
+        PS_ASSERT_ARRAY_NON_NULL((LIST)->y, RETURNVALUE); \
+        PS_ASSERT_ARRAY_NON_NULL((LIST)->flux, RETURNVALUE); \
+        PS_ASSERT_ARRAY_SIZE((LIST)->x, (LIST)->num, RETURNVALUE); \
+        PS_ASSERT_ARRAY_SIZE((LIST)->y, (LIST)->num, RETURNVALUE); \
+        PS_ASSERT_ARRAY_SIZE((LIST)->flux, (LIST)->num, RETURNVALUE); \
+    } \
+}
+
+/// A stamp for image subtraction
+typedef struct {
+    float x, y;                         ///< Position
+    float flux;                         ///< Flux
+    float xNorm, yNorm;                 ///< Normalised position
+    psKernel *reference;                ///< Reference image postage stamp
+    psKernel *input;                    ///< Input image postage stamp
+    psKernel *weight;                   ///< Weight image postage stamp
+    psArray *convolutions;              ///< Convolutions of the reference for each kernel component
+    psImage *matrix;                    ///< Associated matrix
+    psVector *vector;                   ///< Assoicated vector
+    pmSubtractionStampStatus status;    ///< Status of stamp
+} pmSubtractionStamp;
+
+/// Allocate a stamp
+pmSubtractionStamp *pmSubtractionStampAlloc(void);
+
+/// Find stamps on an image
+pmSubtractionStampList *pmSubtractionStampsFind(pmSubtractionStampList *stamps, ///< Output stamps, or NULL
+                                                const psImage *image, ///< Image for which to find stamps
+                                                const psImage *mask, ///< Mask, or NULL
+                                                const psRegion *region, ///< Region to search, or NULL
+                                                float threshold, ///< Threshold for stamps in the image
+                                                float spacing ///< Rough spacing for stamps
+    );
+
+/// Set stamps based on a list of x,y
+///
+/// We may optionally apply an exclusion zone around each star --- this is important when we're convolving to
+/// a specified PSF; less so when we're only trying to get a good subtraction.
+pmSubtractionStampList *pmSubtractionStampsSet(const psVector *x, ///< x coordinates for each stamp
+                                               const psVector *y, ///< y coordinates for each stamp
+                                               const psVector *flux, ///< Flux for each stamp, or NULL
+                                               const psImage *image, ///< Image for flux of stamp
+                                               const psImage *mask, ///< Mask, or NULL
+                                               const psRegion *region, ///< Region to search, or NULL
+                                               float spacing, ///< Rough spacing for stamps
+                                               int exclusionZone ///< Exclusion zone around each star
+    );
+
+///
+pmSubtractionStampList *pmSubtractionStampsSetFromSources(
+    const psArray *sources, ///< Sources for each stamp
+    const psImage *subMask, ///< Mask, or NULL
+    const psRegion *region, ///< Region to search, or NULL
+    float spacing, ///< Rough spacing for stamps
+    int exclusionZone ///< Exclusion zone around each star
+    );
+
+/// Extract stamps from the images
+bool pmSubtractionStampsExtract(pmSubtractionStampList *stamps, ///< Stamps
+                                psImage *reference, ///< Reference image
+                                psImage *input, ///< Input image (or NULL)
+                                psImage *weight, ///< Weight (variance) map
+                                int footprint, ///< Stamp footprint size
+                                int kernelSize ///< Kernel half-size
+    );
+
+/// Generate target for stamps based on list
+bool pmSubtractionStampsGenerate(pmSubtractionStampList *stamps, ///< Stamps
+                                 float fwhm, ///< FWHM for each stamp
+                                 int footprint, ///< Stamp footprint size
+                                 int size ///< Kernel half-size
+    );
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/mainpage.dox
===================================================================
--- /branches/eam_branch_20071023/psModules/src/mainpage.dox	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/mainpage.dox	(revision 15358)
@@ -0,0 +1,127 @@
+/** @mainpage psModules Image Processing Library
+
+
+@section intro Introduction
+This library contains the Pan-STARRS Image Processing Pipeline (IPP) modules (psModules). These modules
+use the functionality of the Pan-STARRS Library (psLib) to perform more complex tasks associated with image
+processing. Modules were constructed to support each of the required processing stages and are listed according
+to the particular processing stage for which they will primarily be used. To preserve namespace, globally-visible
+structures and functions are prefixed with pm, an abbreviation for Pan-STARRS Modules.
+
+The capabilities provided by psModules are grouped into the following areas which are
+also reflected in the file system directory structure:
+ - Configuration
+ - Astrometry, Focal Plane
+ - Photometry
+ - Basic Image Detrending
+ - Object Detection and Classification
+ - Image Combination
+ - Image Subtraction
+
+The installed code for psModules consists of header files and a binary library.
+
+@section extinstall Required external Libraries
+
+Before building psLib from source, several external software libraries must
+be installed. These include:
+ - Pan-STARRS Library (psLib)
+     - Available from the Maui High Performance Computiong Center (MHPCC) at https://mhpcc.pan-starrs.org/code/releases
+     - Compatibility tested with rel5
+     - Note, psLib itself has many external library dependencies.
+ - Doxygen Documentation System
+     - Only needed if code documentation is to be made
+     - Available at http://www.doxygen.org
+     - Compatibility tested with version 1.3.6
+     - The optional companion package, GraphViz (a.k.a., the 'dot' command), is recommended to enable header-file dependency diagrams
+
+We recommend using the particular versions listed as compatibility tested, as
+that is the only versions of the external libraries tested to work well with psLib
+and psModules.  Though it is quite possible that later versions of the libraries
+listed will also work, care must be taken when upgrading these libraries to verify
+that its functionality is compatible with the tested version.
+
+@section install How to Build from Source
+
+Tested versions of psModules are put into a tar file and can be downloaded from:
+
+https://mhpcc.pan-starrs.org/code/releases
+
+If one has a login account on mhpcc.pan-starrs.org, direct CVS access is
+possible.  Example of the commands required for direct CVS retrieval are
+as follows:
+<pre>
+$ cvs -d:ext:USERNAME@mhpcc.pan-starrs.org:/data/panstarrs/cvsroot co -r RELEASEBRANCH psModule
+</pre>
+where:
+  - USERNAME is your login name on the server
+  - RELEASEBRANCH is the desired release branch, e.g. rel7.
+
+
+@section build How to Build and Test the psModules Library.
+
+The psModules library and associated tests are made via the GNU autoconf/automake system.
+
+The source should build using the configure script in the psModules directory.  The
+recommended steps are:
+<pre>
+$ cd psmodules
+$ ./configure
+$ make
+$ make check
+$ make install
+</pre>
+<i>Unless otherwise specified, the library is installed with PREFIX of the current directory.</i>
+
+If the code was retrieved from CVS, you will need to substitute 'autogen.sh' for 'configure' in
+above example.
+
+Other configuration options, such as location of external libraries, are also available.
+To get a list of options, type the following in the top psModules directory.
+<pre>
+$ configure --help
+</pre>
+A likely option needed is '--with-pslib-config', which specifies the location of
+the configuration script for psLib.  By default, configure searches for it using PATH, but that
+is not always sufficient.
+
+@section install How to Install
+
+To install the library using the prefix given in the configure step, execute in
+the top build directory:
+<pre>
+$ make install
+</pre>
+
+
+@section usage Building and Linking your code to the psModules library
+
+To assist the use of the library with your own code, a configuration tool is part
+of the psModules library package.  This tool, psmodules-config, is installed in the BIN
+directory, according to the options given to the configure script.
+
+The required CFLAG options for the compiler stage of code that uses psModules can be
+obtained via 'psmodules-config --cflags'.  This outputs the cc options that supplies
+include path(s) required to find the psModules headers.
+
+The required linking options, can be obtained via 'psmodules-config --libs'.  This
+outputs the ld options that supplies the library paths and files required to
+link to the psModules library.
+
+Note: psmodules-config usage above refers to the install locations of the library.  
+
+@section doc How to Create Code Documentation
+
+Both HTML and man page documentation may be generated from the inline
+documentation embedded in the code using the following commands:
+<pre>
+$ cd psmodules
+$ make docs
+</pre>
+<i>This places documentation in PREFIX/docs.</i>
+
+Also, a prebuilt set of code documentation for both the releases and last
+CVS snapshot can be found at:
+
+https://mhpcc.pan-starrs.org/docs/
+
+*/
Index: /branches/eam_branch_20071023/psModules/src/objects/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/.cvsignore	(revision 15358)
@@ -0,0 +1,6 @@
+.deps
+.libs
+Makefile
+Makefile.in
+*.la
+*.lo
Index: /branches/eam_branch_20071023/psModules/src/objects/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/Makefile.am	(revision 15358)
@@ -0,0 +1,67 @@
+noinst_LTLIBRARIES = libpsmodulesobjects.la
+
+libpsmodulesobjects_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS) -I../pslib/
+libpsmodulesobjects_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulesobjects_la_SOURCES  = \
+     pmPeaks.c \
+     pmMoments.c \
+     pmModel.c \
+     pmModelClass.c \
+     pmModelUtils.c \
+     pmSource.c \
+     pmSourceUtils.c \
+     pmSourceSky.c \
+     pmSourceContour.c \
+     pmSourceFitModel.c \
+     pmSourceFitSet.c \
+     pmSourcePhotometry.c \
+     pmSourceIO.c \
+     pmSourceIO_RAW.c \
+     pmSourceIO_OBJ.c \
+     pmSourceIO_SX.c \
+     pmSourceIO_CMP.c \
+     pmSourceIO_SMPDATA.c \
+     pmSourceIO_PS1_DEV_0.c \
+     pmSourceIO_PS1_DEV_1.c \
+     pmSourcePlots.c \
+     pmSourcePlotPSFModel.c \
+     pmSourcePlotMoments.c \
+     pmSourcePlotApResid.c \
+     pmResiduals.c \
+     pmPSF.c \
+     pmPSF_IO.c \
+     pmPSFtry.c \
+     pmTrend2D.c \
+     pmGrowthCurve.c
+
+EXTRA_DIST = \
+     models/pmModel_GAUSS.c \
+     models/pmModel_PGAUSS.c \
+     models/pmModel_QGAUSS.c \
+     models/pmModel_SGAUSS.c \
+     models/pmModel_RGAUSS.c \
+     models/pmModel_SERSIC.c
+
+pkginclude_HEADERS = \
+     pmPeaks.h \
+     pmMoments.h \
+     pmModel.h \
+     pmModelClass.h \
+     pmModelUtils.h \
+     pmSource.h \
+     pmSourceUtils.h \
+     pmSourceSky.h \
+     pmSourceContour.h \
+     pmSourceFitModel.h \
+     pmSourceFitSet.h \
+     pmSourcePhotometry.h \
+     pmSourceIO.h \
+     pmSourcePlots.h \
+     pmResiduals.h \
+     pmPSF.h \
+     pmPSF_IO.h \
+     pmPSFtry.h \
+     pmTrend2D.h \
+     pmGrowthCurve.h
+
+CLEANFILES = *~
Index: /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_GAUSS.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_GAUSS.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_GAUSS.c	(revision 15358)
@@ -0,0 +1,396 @@
+/******************************************************************************
+ * this file defines the GAUSS source shape model.  Note that these model functions are loaded
+ * by pmModelGroup.c using 'include', and thus need no 'include' statements of their own.  The
+ * models use a psVector to represent the set of parameters, with the sequence used to specify
+ * the meaning of the parameter.  The meaning of the parameters may thus vary depending on the
+ * specifics of the model.  All models which are used a PSF representations share a few
+ * parameters, for which # define names are listed in pmModel.h:
+ 
+   pure Gaussian:
+   exp(-z)
+ 
+ * PM_PAR_SKY 0   - local sky : note that this is unused and may be dropped in the future
+ * PM_PAR_I0 1    - central intensity
+ * PM_PAR_XPOS 2  - X center of object
+ * PM_PAR_YPOS 3  - Y center of object
+ * PM_PAR_SXX 4   - X^2 term of elliptical contour (sqrt(2) * SigmaX)
+ * PM_PAR_SYY 5   - Y^2 term of elliptical contour (sqrt(2) * SigmaY)
+ * PM_PAR_SXY 6   - X*Y term of elliptical contour
+ *****************************************************************************/
+
+# define PM_MODEL_FUNC       	  pmModelFunc_GAUSS
+# define PM_MODEL_FLUX       	  pmModelFlux_GAUSS
+# define PM_MODEL_GUESS      	  pmModelGuess_GAUSS
+# define PM_MODEL_LIMITS     	  pmModelLimits_GAUSS
+# define PM_MODEL_RADIUS     	  pmModelRadius_GAUSS
+# define PM_MODEL_FROM_PSF   	  pmModelFromPSF_GAUSS
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_GAUSS
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_GAUSS
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+psF32 PM_MODEL_FUNC(psVector *deriv,
+                    const psVector *params,
+                    const psVector *pixcoord)
+{
+    psF32 *PAR = params->data.F32;
+
+    psF32 X  = pixcoord->data.F32[0] - PAR[PM_PAR_XPOS];
+    psF32 Y  = pixcoord->data.F32[1] - PAR[PM_PAR_YPOS];
+    psF32 px = X / PAR[PM_PAR_SXX];
+    psF32 py = Y / PAR[PM_PAR_SYY];
+    psF32 z  = PS_SQR(px) + PS_SQR(py) + PAR[PM_PAR_SXY]*X*Y;
+    assert (z >= 0.0);
+
+    psF32 r  = exp(-z);
+    psF32 q  = PAR[PM_PAR_I0]*r;
+    psF32 f  = q + PAR[PM_PAR_SKY];
+
+    if (deriv != NULL) {
+        psF32 *dPAR = deriv->data.F32;
+        dPAR[PM_PAR_SKY]  = +1.0;
+        dPAR[PM_PAR_I0]   = +r;
+        dPAR[PM_PAR_XPOS] = q*(2*px/PAR[PM_PAR_SXX] + Y*PAR[PM_PAR_SXY]);
+        dPAR[PM_PAR_YPOS] = q*(2*py/PAR[PM_PAR_SYY] + X*PAR[PM_PAR_SXY]);
+        // the extra factor of 2 below is needed to avoid excessive swings
+        dPAR[PM_PAR_SXX]  = +4.0*q*px*px/PAR[PM_PAR_SXX];
+        dPAR[PM_PAR_SYY]  = +4.0*q*py*py/PAR[PM_PAR_SYY];
+        dPAR[PM_PAR_SXY]  = -q*X*Y;
+    }
+    return(f);
+}
+
+// define the parameter limits
+// AR_MAX is the maximum allowed axis ratio
+// AR_RATIO is ((1-R)/(1+R))^2 where R = AR_MAX^(-2)
+# define AR_MAX 20.0
+# define AR_RATIO 0.99
+
+bool PM_MODEL_LIMITS (psMinConstraintMode mode, int nParam, float *params, float *beta)
+{
+    float beta_lim = 0, params_min = 0, params_max = 0;
+    float f1 = 0, f2 = 0, q1 = 0, q2 = 0;
+
+    // we need to calculate the limits for SXY specially
+    if (nParam == PM_PAR_SXY) {
+        f1 = 1.0 / PS_SQR(params[PM_PAR_SYY]) + 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        f2 = 1.0 / PS_SQR(params[PM_PAR_SYY]) - 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        q1 = PS_SQR(f1)*AR_RATIO - PS_SQR(f2);
+	q1 = PS_MAX (0.0, q1); 
+	// if q1 < 0.0, f2 ~ f1, we have a very large axis ratio near 45deg..  Saturate at that
+	// angle and let f2,f1 fight it out
+        q2  = 0.5*sqrt (q1);
+    }
+
+    switch (mode) {
+    case PS_MINIMIZE_BETA_LIMIT:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            beta_lim = 1000;
+            break;
+        case PM_PAR_I0:
+            beta_lim = 3e6;
+            break;
+        case PM_PAR_XPOS:
+            beta_lim = 5;
+            break;
+        case PM_PAR_YPOS:
+            beta_lim = 5;
+            break;
+        case PM_PAR_SXX:
+            beta_lim = 2.0;
+            break;
+        case PM_PAR_SYY:
+            beta_lim = 2.0;
+            break;
+        case PM_PAR_SXY:
+            beta_lim =  0.5*q2;
+            break;
+        default:
+            psAbort("invalid parameter %d for beta test", nParam);
+        }
+        if (fabs(beta[nParam]) > fabs(beta_lim)) {
+            beta[nParam] = (beta[nParam] > 0) ? fabs(beta_lim) : -fabs(beta_lim);
+	    psTrace ("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+		     nParam, beta[nParam], beta_lim);
+            return false;
+        }
+        return true;
+    case PS_MINIMIZE_PARAM_MIN:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            params_min = -1000;
+            break;
+        case PM_PAR_I0:
+            params_min =   0.01;
+            break;
+        case PM_PAR_XPOS:
+            params_min =  -100;
+            break;
+        case PM_PAR_YPOS:
+            params_min =  -100;
+            break;
+        case PM_PAR_SXX:
+            params_min =   0.5;
+            break;
+        case PM_PAR_SYY:
+            params_min =   0.5;
+            break;
+        case PM_PAR_SXY:
+            params_min =   -q2;
+            break;
+        default:
+            psAbort("invalid parameter %d for param min test", nParam);
+        }
+        if (params[nParam] < params_min) {
+            params[nParam] = params_min;
+	    psTrace ("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+		     nParam, params[nParam], params_min);
+            return false;
+        }
+        return true;
+    case PS_MINIMIZE_PARAM_MAX:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            params_max =   1e5;
+            break;
+        case PM_PAR_I0:
+            params_max =   1e8;
+            break;
+        case PM_PAR_XPOS:
+            params_max =   1e4;
+            break;
+        case PM_PAR_YPOS:
+            params_max =   1e4;
+            break;
+        case PM_PAR_SXX:
+            params_max =   100;
+            break;
+        case PM_PAR_SYY:
+            params_max =   100;
+            break;
+        case PM_PAR_SXY:
+            params_max =   +q2;
+            break;
+        default:
+            psAbort("invalid parameter %d for param max test", nParam);
+        }
+        if (params[nParam] > params_max) {
+            params[nParam] = params_max;
+	    psTrace ("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+		     nParam, params[nParam], params_max);
+            return false;
+        }
+        return true;
+    default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+// make an initial guess for parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source)
+{
+    pmMoments *moments = source->moments;
+    psF32     *PAR  = model->params->data.F32;
+
+    psEllipseMoments emoments;
+    emoments.x2 = moments->Sx;
+    emoments.y2 = moments->Sy;
+    emoments.xy = moments->Sxy;
+
+    // force the axis ratio to be < 20.0
+    psEllipseAxes axes = psEllipseMomentsToAxes (emoments, 20.0);
+    psEllipseShape shape = psEllipseAxesToShape (axes);
+
+    PAR[PM_PAR_SKY]  = moments->Sky;
+    PAR[PM_PAR_I0]   = moments->Peak - moments->Sky;
+    PAR[PM_PAR_XPOS] = moments->x;
+    PAR[PM_PAR_YPOS] = moments->y;
+    PAR[PM_PAR_SXX] = PS_MAX(0.5, M_SQRT2*shape.sx);
+    PAR[PM_PAR_SYY] = PS_MAX(0.5, M_SQRT2*shape.sy);
+    PAR[PM_PAR_SXY] = shape.sxy;
+    return(true);
+}
+
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+
+    psEllipseShape shape;
+
+    psF32 *PAR = params->data.F32;
+
+    shape.sx  = PAR[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = PAR[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    // Area is equivalent to 2 pi sigma^2
+    // axes ratio < 20
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 Area = 2.0 * M_PI * axes.major * axes.minor;
+
+    psF64 Flux = params->data.F32[PM_PAR_I0] * Area;
+
+    return(Flux);
+}
+
+// return the radius which yields the requested flux
+// this function is never allowed to return <= 0
+psF64 PM_MODEL_RADIUS (const psVector *params, psF64 flux)
+{
+    psEllipseShape shape;
+
+    psF32 *PAR = params->data.F32;
+
+    if (flux <= 0)
+        return (1.0);
+    if (PAR[PM_PAR_I0] <= 0)
+        return (1.0);
+    if (flux >= PAR[PM_PAR_I0])
+        return (1.0);
+
+    shape.sx  = PAR[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = PAR[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 radius = axes.major * sqrt (2.0 * log(PAR[PM_PAR_I0] / flux));
+    return (radius);
+}
+
+// construct the PSF model from the FLT model and the psf
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, pmPSF *psf)
+{
+    psF32 *out = modelPSF->params->data.F32;
+    psF32 *in  = modelFLT->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    // supply the model-fitted parameters, or copy from the input
+    for (int i = 0; i < psf->params->n; i++) {
+        if (psf->params->data[i] == NULL) {
+            out[i] = in[i];
+        } else {
+            pmTrend2D *trend = psf->params->data[i];
+            out[i] = pmTrend2DEval(trend, in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
+        }
+    }
+
+    // the OLD 2D model for SXY actually fitted SXY / (SXX^-2 + SYY^-2); correct here
+    // out[PM_PAR_SXY] = pmPSF_SXYtoModel (out);
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    // XXX user-defined value for limit?
+    if (!pmPSF_FitToModel (out, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)",
+		in[PM_PAR_YPOS], in[PM_PAR_XPOS]);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MIN, i, out, NULL);
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MAX, i, out, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)",
+		     in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
+	    modelPSF->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+    return(true);
+}
+
+// construct the PSF model from the FLT model and the psf
+// XXX is this sufficiently general do be a global function, not a pmModelClass function?
+bool PM_MODEL_PARAMS_FROM_PSF (pmModel *model, pmPSF *psf, float Xo, float Yo, float Io)
+{
+    psF32 *PAR = model->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    PAR[PM_PAR_SKY]  = 0.0;
+    PAR[PM_PAR_I0]   = Io;
+    PAR[PM_PAR_XPOS] = Xo;
+    PAR[PM_PAR_YPOS] = Yo;
+    
+    // supply the model-fitted parameters, or copy from the input
+    for (int i = 0; i < psf->params->n; i++) {
+	if (i == PM_PAR_SKY) continue;
+	if (i == PM_PAR_I0) continue;
+	if (i == PM_PAR_XPOS) continue;
+	if (i == PM_PAR_YPOS) continue;
+	pmTrend2D *trend = psf->params->data[i];
+	PAR[i] = pmTrend2DEval(trend, Xo, Yo);
+    }
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    // XXX user-defined value for limit?
+    if (!pmPSF_FitToModel (PAR, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)", Xo, Yo);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MIN, i, PAR, NULL);
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MAX, i, PAR, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)", Xo, Yo);
+	    model->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+    return(true);
+}
+
+// check the status of the fitted model
+// this test is invalid if the parameters are derived
+// from the PSF model
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+    psF32 dP;
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    dP = 0;
+    dP += PS_SQR(dPAR[PM_PAR_SXX] / PAR[PM_PAR_SXX]);
+    dP += PS_SQR(dPAR[PM_PAR_SYY] / PAR[PM_PAR_SYY]);
+    dP = sqrt (dP);
+
+    status = true;
+    status &= (dP < 0.5);
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    if (status)
+        return true;
+    return false;
+}
+
+# undef PM_MODEL_FUNC
+# undef PM_MODEL_FLUX
+# undef PM_MODEL_GUESS
+# undef PM_MODEL_LIMITS
+# undef PM_MODEL_RADIUS
+# undef PM_MODEL_FROM_PSF
+# undef PM_MODEL_PARAMS_FROM_PSF
+# undef PM_MODEL_FIT_STATUS
Index: /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_PGAUSS.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_PGAUSS.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_PGAUSS.c	(revision 15358)
@@ -0,0 +1,445 @@
+/******************************************************************************
+ * this file defines the PGAUSS source shape model.  Note that these model functions are loaded
+ * by pmModelGroup.c using 'include', and thus need no 'include' statements of their own.  The
+ * models use a psVector to represent the set of parameters, with the sequence used to specify
+ * the meaning of the parameter.  The meaning of the parameters may thus vary depending on the
+ * specifics of the model.  All models which are used a PSF representations share a few
+ * parameters, for which # define names are listed in pmModel.h:
+ 
+   Gaussian taylor expansion
+   1 / (1 + z + z^2/2 + z^3/6)
+ 
+ * PM_PAR_SKY 0   - local sky : note that this is unused and may be dropped in the future
+ * PM_PAR_I0 1    - central intensity
+ * PM_PAR_XPOS 2  - X center of object
+ * PM_PAR_YPOS 3  - Y center of object
+ * PM_PAR_SXX 4   - X^2 term of elliptical contour (sqrt(2) * SigmaX)
+ * PM_PAR_SYY 5   - Y^2 term of elliptical contour (sqrt(2) * SigmaY)
+ * PM_PAR_SXY 6   - X*Y term of elliptical contour
+ *****************************************************************************/
+
+# define PM_MODEL_FUNC       	  pmModelFunc_PGAUSS
+# define PM_MODEL_FLUX       	  pmModelFlux_PGAUSS
+# define PM_MODEL_GUESS      	  pmModelGuess_PGAUSS
+# define PM_MODEL_LIMITS     	  pmModelLimits_PGAUSS
+# define PM_MODEL_RADIUS     	  pmModelRadius_PGAUSS
+# define PM_MODEL_FROM_PSF   	  pmModelFromPSF_PGAUSS
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_PGAUSS
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_PGAUSS
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+psF32 PM_MODEL_FUNC(psVector *deriv,
+                    const psVector *params,
+                    const psVector *pixcoord)
+{
+    psF32 *PAR = params->data.F32;
+
+    psF32 X  = pixcoord->data.F32[0] - PAR[PM_PAR_XPOS];
+    psF32 Y  = pixcoord->data.F32[1] - PAR[PM_PAR_YPOS];
+    psF32 px = X / PAR[PM_PAR_SXX];
+    psF32 py = Y / PAR[PM_PAR_SYY];
+    psF32 z  = PS_SQR(px) + PS_SQR(py) + PAR[PM_PAR_SXY]*X*Y;
+    assert (z >= 0.0);
+
+    psF32 t  = 1 + z + z*z/2.0;
+    psF32 r  = 1.0 / (t + z*z*z/6.0); /* exp (-Z) */
+    psF32 f  = PAR[PM_PAR_I0]*r + PAR[PM_PAR_SKY];
+
+    if (deriv != NULL) {
+        psF32 *dPAR = deriv->data.F32;
+        psF32 q = PAR[PM_PAR_I0]*r*r*t;
+        dPAR[PM_PAR_SKY] = +1.0;
+        dPAR[PM_PAR_I0] = +r;
+        dPAR[PM_PAR_XPOS] = q*(2.0*px/PAR[PM_PAR_SXX] + Y*PAR[PM_PAR_SXY]);
+        dPAR[PM_PAR_YPOS] = q*(2.0*py/PAR[PM_PAR_SYY] + X*PAR[PM_PAR_SXY]);
+        // the extra factor of 2 below is needed to avoid excessive swings
+        dPAR[PM_PAR_SXX] =  +4.0*q*px*px/PAR[PM_PAR_SXX];
+        dPAR[PM_PAR_SYY] =  +4.0*q*py*py/PAR[PM_PAR_SYY];
+        dPAR[PM_PAR_SXY] = -q*X*Y;
+    }
+    return(f);
+}
+
+// define the parameter limits
+// AR_MAX is the maximum allowed axis ratio
+// AR_RATIO is ((1-R)/(1+R))^2 where R = AR_MAX^(-2)
+# define AR_MAX 20.0
+# define AR_RATIO 0.99
+
+bool PM_MODEL_LIMITS (psMinConstraintMode mode, int nParam, float *params, float *beta)
+{
+    float beta_lim = 0, params_min = 0, params_max = 0;
+    float f1 = 0, f2 = 0, q1 = 0, q2 = 0;
+
+    // we need to calculate the limits for SXY specially
+    if (nParam == PM_PAR_SXY) {
+        f1 = 1.0 / PS_SQR(params[PM_PAR_SYY]) + 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        f2 = 1.0 / PS_SQR(params[PM_PAR_SYY]) - 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        q1 = PS_SQR(f1)*AR_RATIO - PS_SQR(f2);
+	q1 = PS_MAX (0.0, q1); 
+	// if q1 < 0.0, f2 ~ f1, we have a very large axis ratio near 45deg..  Saturate at that
+	// angle and let f2,f1 fight it out
+        q2  = 0.5*sqrt (q1);
+    }
+
+    switch (mode) {
+    case PS_MINIMIZE_BETA_LIMIT:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            beta_lim = 1000;
+            break;
+        case PM_PAR_I0:
+            beta_lim = 3e6;
+            break;
+        case PM_PAR_XPOS:
+            beta_lim = 5;
+            break;
+        case PM_PAR_YPOS:
+            beta_lim = 5;
+            break;
+        case PM_PAR_SXX:
+            beta_lim = 2.0;
+            break;
+        case PM_PAR_SYY:
+            beta_lim = 2.0;
+            break;
+        case PM_PAR_SXY:
+            beta_lim =  0.5*q2;
+            break;
+        default:
+            psAbort("invalid parameter %d for beta test", nParam);
+        }
+        if (fabs(beta[nParam]) > fabs(beta_lim)) {
+            beta[nParam] = (beta[nParam] > 0) ? fabs(beta_lim) : -fabs(beta_lim);
+	    psTrace ("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+		     nParam, beta[nParam], beta_lim);
+            return false;
+        }
+        return true;
+    case PS_MINIMIZE_PARAM_MIN:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            params_min = -1000;
+            break;
+        case PM_PAR_I0:
+            params_min =  0.01;
+            break;
+        case PM_PAR_XPOS:
+            params_min =  -100;
+            break;
+        case PM_PAR_YPOS:
+            params_min =  -100;
+            break;
+        case PM_PAR_SXX:
+            params_min =   0.5;
+            break;
+        case PM_PAR_SYY:
+            params_min =   0.5;
+            break;
+        case PM_PAR_SXY:
+            params_min =   -q2;
+            break;
+        default:
+            psAbort("invalid parameter %d for param min test", nParam);
+        }
+	if (params[nParam] < params_min) {
+            params[nParam] = params_min;
+	    psTrace ("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+		     nParam, params[nParam], params_min);
+            return false;
+        }
+        return true;
+    case PS_MINIMIZE_PARAM_MAX:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            params_max =   1e5;
+            break;
+        case PM_PAR_I0:
+            params_max =   1e8;
+            break;
+        case PM_PAR_XPOS:
+            params_max =   1e4;
+            break;
+        case PM_PAR_YPOS:
+            params_max =   1e4;
+            break;
+        case PM_PAR_SXX:
+            params_max =   100;
+            break;
+        case PM_PAR_SYY:
+            params_max =   100;
+            break;
+        case PM_PAR_SXY:
+            params_max =   +q2;
+            break;
+        default:
+            psAbort("invalid parameter %d for param max test", nParam);
+        }
+        if (params[nParam] > params_max) {
+            params[nParam] = params_max;
+	    psTrace ("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+		     nParam, params[nParam], params_max);
+            return false;
+        }
+        return true;
+    default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+// make an initial guess for parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source)
+{
+    pmMoments *moments = source->moments;
+    psF32     *PAR     = model->params->data.F32;
+
+    psEllipseMoments emoments;
+    emoments.x2 = moments->Sx;
+    emoments.y2 = moments->Sx;
+    emoments.xy = moments->Sxy;
+
+    psEllipseAxes axes = psEllipseMomentsToAxes (emoments, 20.0);
+    psEllipseShape shape = psEllipseAxesToShape (axes);
+
+    PAR[PM_PAR_SKY]  = moments->Sky;
+    PAR[PM_PAR_I0]   = moments->Peak - moments->Sky;
+    PAR[PM_PAR_XPOS] = moments->x; // XXX use peak->xf, peak->yf?
+    PAR[PM_PAR_YPOS] = moments->y;
+    PAR[PM_PAR_SXX] = PS_MAX(0.5, M_SQRT2*shape.sx);
+    PAR[PM_PAR_SYY] = PS_MAX(0.5, M_SQRT2*shape.sy);
+    PAR[PM_PAR_SXY] = shape.sxy;
+    return(true);
+}
+
+psF64 PM_MODEL_FLUX(const psVector *params)
+{
+    float norm, z;
+    psEllipseShape shape;
+
+    psF32 *PAR = params->data.F32;
+
+    shape.sx  = PAR[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = PAR[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    // Area is equivalent to 2 pi sigma^2
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 Area = 2.0 * M_PI * axes.major * axes.minor;
+
+    // the area needs to be multiplied by the integral of f(z)
+    norm = 0.0;
+
+    # define DZ 0.25
+
+    float f0 = 1.0;
+    float f1, f2;
+    for (z = DZ; z < 50; z += DZ) {
+        f1 = 1.0 / (1 + z + z*z/2.0 + z*z*z/6.0);
+        z += DZ;
+        f2 = 1.0 / (1 + z + z*z/2.0 + z*z*z/6.0);
+        norm += f0 + 4*f1 + f2;
+        f0 = f2;
+    }
+    norm *= DZ / 3.0;
+
+    psF64 Flux = PAR[PM_PAR_I0] * Area * norm;
+
+    return(Flux);
+}
+
+// define this function so it never returns Inf or NaN
+// return the radius which yields the requested flux
+psF64 PM_MODEL_RADIUS (const psVector *params, psF64 flux)
+{
+    psF64 z, f;
+    int Nstep = 0;
+    psEllipseShape shape;
+
+    psF32 *PAR = params->data.F32;
+
+    if (flux <= 0)
+        return (1.0);
+    if (PAR[PM_PAR_I0] <= 0)
+        return (1.0);
+    if (flux >= PAR[PM_PAR_I0])
+        return (1.0);
+
+    shape.sx  = PAR[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = PAR[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    // this estimates the radius assuming f(z) is roughly exp(-z)
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 sigma = axes.major;
+
+    psF64 limit = flux / PAR[PM_PAR_I0];
+
+    // use the fact that f is monotonically decreasing
+    z = 0;
+    Nstep = 0;
+
+    // choose a z value guaranteed to be beyond our limit
+    float z0 = pow((1.0 / limit), (1.0 / 3.0));
+    float z1 = (1.0 / limit);
+    z1 = PS_MAX (z0, z1);
+    z0 = 0.0;
+
+    // perform a type of bisection to find the value
+    float f0 = 1.0 / (1 + z0 + z0*z0/2.0 + z0*z0*z0/6.0);
+    float f1 = 1.0 / (1 + z1 + z1*z1/2.0 + z1*z1*z1/6.0);
+    while ((Nstep < 10) && (fabs(z1 - z0) > 0.5)) {
+        z = 0.5*(z0 + z1);
+	f = 1.0 / (1 + z + z*z/2.0 + z*z*z/6.0);
+        if (f > limit) {
+            z0 = z;
+            f0 = f;
+        } else {
+            z1 = z;
+            f1 = f;
+        }
+        Nstep ++;
+    }
+    psF64 radius = sigma * sqrt (2.0 * z);
+
+    // psF64 radius = axes.major * sqrt (2.0 * log(PAR[PM_PAR_I0] / flux));
+
+    if (isnan(radius))
+        psAbort("error in code: radius is NaN");
+    if (radius < 0)
+        psAbort("error in code: radius is negative");
+
+    return (radius);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, pmPSF *psf)
+{
+
+    psF32 *out = modelPSF->params->data.F32;
+    psF32 *in  = modelFLT->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    for (int i = 0; i < psf->params->n; i++) {
+        if (psf->params->data[i] == NULL) {
+            out[i] = in[i];
+        } else {
+            pmTrend2D *trend = psf->params->data[i];
+            out[i] = pmTrend2DEval(trend, in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
+        }
+    }
+
+    // the OLD 2D model for SXY actually fitted SXY / (SXX^-2 + SYY^-2); correct here
+    // out[PM_PAR_SXY] = pmPSF_SXYtoModel (out);
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    if (!pmPSF_FitToModel (out, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)",
+		in[PM_PAR_YPOS], in[PM_PAR_XPOS]);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MIN, i, out, NULL);
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MAX, i, out, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)",
+		     in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
+	    modelPSF->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+    return(true);
+}
+
+// construct the PSF model from the FLT model and the psf
+// XXX is this sufficiently general do be a global function, not a pmModelClass function?
+bool PM_MODEL_PARAMS_FROM_PSF (pmModel *model, pmPSF *psf, float Xo, float Yo, float Io)
+{
+    psF32 *PAR = model->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    PAR[PM_PAR_SKY]  = 0.0;
+    PAR[PM_PAR_I0]   = Io;
+    PAR[PM_PAR_XPOS] = Xo;
+    PAR[PM_PAR_YPOS] = Yo;
+    
+    // supply the model-fitted parameters, or copy from the input
+    for (int i = 0; i < psf->params->n; i++) {
+	if (i == PM_PAR_SKY) continue;
+	if (i == PM_PAR_I0) continue;
+	if (i == PM_PAR_XPOS) continue;
+	if (i == PM_PAR_YPOS) continue;
+	pmTrend2D *trend = psf->params->data[i];
+	PAR[i] = pmTrend2DEval(trend, Xo, Yo);
+    }
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    // XXX user-defined value for limit?
+    if (!pmPSF_FitToModel (PAR, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)", Xo, Yo);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MIN, i, PAR, NULL);
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MAX, i, PAR, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)", Xo, Yo);
+	    model->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+    return(true);
+}
+
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+
+    psF32 dP;
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    dP = 0;
+    dP += PS_SQR(dPAR[PM_PAR_SXX] / PAR[PM_PAR_SXX]);
+    dP += PS_SQR(dPAR[PM_PAR_SYY] / PAR[PM_PAR_SYY]);
+    dP = sqrt (dP);
+
+    status = true;
+    status &= (dP < 0.5);
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+# undef PM_MODEL_FUNC
+# undef PM_MODEL_FLUX
+# undef PM_MODEL_GUESS
+# undef PM_MODEL_LIMITS
+# undef PM_MODEL_RADIUS
+# undef PM_MODEL_FROM_PSF
+# undef PM_MODEL_PARAMS_FROM_PSF
+# undef PM_MODEL_FIT_STATUS
Index: /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_QGAUSS.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_QGAUSS.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_QGAUSS.c	(revision 15358)
@@ -0,0 +1,473 @@
+/******************************************************************************
+ * this file defines the QGAUSS source shape model (XXX need a better name!).  Note that these
+ * model functions are loaded by pmModelGroup.c using 'include', and thus need no 'include'
+ * statements of their own.  The models use a psVector to represent the set of parameters, with
+ * the sequence used to specify the meaning of the parameter.  The meaning of the parameters
+ * may thus vary depending on the specifics of the model.  All models which are used a PSF
+ * representations share a few parameters, for which # define names are listed in pmModel.h:
+ 
+   power-law with fitted linear term
+   1 / (1 + kz + z^2.25)
+ 
+   * PM_PAR_SKY 0   - local sky : note that this is unused and may be dropped in the future
+   * PM_PAR_I0 1    - central intensity
+   * PM_PAR_XPOS 2  - X center of object
+   * PM_PAR_YPOS 3  - Y center of object
+   * PM_PAR_SXX 4   - X^2 term of elliptical contour (sqrt(2) / SigmaX)
+   * PM_PAR_SYY 5   - Y^2 term of elliptical contour (sqrt(2) / SigmaY)
+   * PM_PAR_SXY 6   - X*Y term of elliptical contour
+   * PM_PAR_7   7   - amplitude of the linear component (k)
+   *****************************************************************************/
+
+# define PM_MODEL_FUNC       	  pmModelFunc_QGAUSS
+# define PM_MODEL_FLUX       	  pmModelFlux_QGAUSS
+# define PM_MODEL_GUESS      	  pmModelGuess_QGAUSS
+# define PM_MODEL_LIMITS     	  pmModelLimits_QGAUSS
+# define PM_MODEL_RADIUS     	  pmModelRadius_QGAUSS
+# define PM_MODEL_FROM_PSF   	  pmModelFromPSF_QGAUSS
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_QGAUSS
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_QGAUSS
+
+psF32 PM_MODEL_FUNC (psVector *deriv,
+                     const psVector *params,
+                     const psVector *pixcoord)
+{
+    psF32 *PAR = params->data.F32;
+
+    psF32 X  = pixcoord->data.F32[0] - PAR[PM_PAR_XPOS];
+    psF32 Y  = pixcoord->data.F32[1] - PAR[PM_PAR_YPOS];
+    psF32 px = X / PAR[PM_PAR_SXX];
+    psF32 py = Y / PAR[PM_PAR_SYY];
+    psF32 z  = PS_SQR(px) + PS_SQR(py) + PAR[PM_PAR_SXY]*X*Y;
+
+    // XXX if the elliptical contour is defined in valid way, this step should not be required.
+    // other models (like PGAUSS) don't use fractional powers, and thus do not have NaN values
+    // for negative values of z
+    // XXX use an assert here to force the elliptical parameters to be correctly determined
+    // if (z < 0) z = 0;
+    assert (z >= 0);
+
+    psF32 zp = pow(z,1.25);
+    psF32 r  = 1.0 / (1 + PAR[PM_PAR_7]*z + z*zp);
+
+    psF32 r1 = PAR[PM_PAR_I0]*r;
+    psF32 f  = r1 + PAR[PM_PAR_SKY];
+
+    if (deriv != NULL) {
+        psF32 *dPAR = deriv->data.F32;
+
+        // note difference from a pure gaussian: q = params->data.F32[PM_PAR_I0]*r
+        psF32 t = r1*r;
+        psF32 q = t*(PAR[PM_PAR_7] + 2.25*zp);
+
+        dPAR[PM_PAR_SKY]  = +1.0;
+        dPAR[PM_PAR_I0]   = +r;
+        dPAR[PM_PAR_XPOS] = q*(2.0*px/PAR[PM_PAR_SXX] + Y*PAR[PM_PAR_SXY]);
+        dPAR[PM_PAR_YPOS] = q*(2.0*py/PAR[PM_PAR_SYY] + X*PAR[PM_PAR_SXY]);
+        // the extra factor of 2 below is needed to avoid excessive swings
+        dPAR[PM_PAR_SXX]  = +4.0*q*px*px/PAR[PM_PAR_SXX];
+        dPAR[PM_PAR_SYY]  = +4.0*q*py*py/PAR[PM_PAR_SYY];
+        dPAR[PM_PAR_SXY]  = -q*X*Y;
+        dPAR[PM_PAR_7]    = -t*z;
+    }
+    return(f);
+}
+
+// define the parameter limits
+// AR_MAX is the maximum allowed axis ratio
+// AR_RATIO is ((1-R)/(1+R))^2 where R = AR_MAX^(-2)
+# define AR_MAX 20.0
+# define AR_RATIO 0.99
+bool PM_MODEL_LIMITS (psMinConstraintMode mode, int nParam, float *params, float *beta)
+{
+    float beta_lim = 0, params_min = 0, params_max = 0;
+    float f1 = 0, f2 = 0, q1 = 0, q2 = 0;
+
+    // we need to calculate the limits for SXY specially
+    if (nParam == PM_PAR_SXY) {
+        f1 = 1.0 / PS_SQR(params[PM_PAR_SYY]) + 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        f2 = 1.0 / PS_SQR(params[PM_PAR_SYY]) - 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        q1 = PS_SQR(f1)*AR_RATIO - PS_SQR(f2);
+	q1 = (q1 < 0.0) ? 0.0 : q1;
+	// if q1 < 0.0, f2 ~ f1, we have a very large axis ratio near 45deg..  Saturate at that
+	// angle and let f2,f1 fight it out
+        q2  = 0.5*sqrt (q1);
+    }
+
+    switch (mode) {
+    case PS_MINIMIZE_BETA_LIMIT:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            beta_lim = 1000;
+            break;
+        case PM_PAR_I0:
+            beta_lim = 3e6;
+            break;
+        case PM_PAR_XPOS:
+            beta_lim = 5;
+            break;
+        case PM_PAR_YPOS:
+            beta_lim = 5;
+            break;
+        case PM_PAR_SXX:
+            beta_lim = 1.0;
+            break;
+        case PM_PAR_SYY:
+            beta_lim = 1.0;
+            break;
+        case PM_PAR_SXY:
+            beta_lim =  0.5*q2;
+            break;
+        case PM_PAR_7:
+            beta_lim = 2.0;
+            break;
+        default:
+            psAbort("invalid parameter %d for beta test", nParam);
+        }
+        if (fabs(beta[nParam]) > fabs(beta_lim)) {
+            beta[nParam] = (beta[nParam] > 0) ? fabs(beta_lim) : -fabs(beta_lim);
+	    psTrace ("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+		     nParam, beta[nParam], beta_lim);
+            return false;
+        }
+        return true;
+    case PS_MINIMIZE_PARAM_MIN:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            params_min = -1000;
+            break;
+        case PM_PAR_I0:
+            params_min =   0.01;
+            break;
+        case PM_PAR_XPOS:
+            params_min =  -100;
+            break;
+        case PM_PAR_YPOS:
+            params_min =  -100;
+            break;
+        case PM_PAR_SXX:
+            params_min =   0.5;
+            break;
+        case PM_PAR_SYY:
+            params_min =   0.5;
+            break;
+        case PM_PAR_SXY:
+            params_min =  -q2;
+            break;
+        case PM_PAR_7:
+            params_min =   0.1;
+            break;
+        default:
+            psAbort("invalid parameter %d for param min test", nParam);
+        }
+        if (params[nParam] < params_min) {
+            params[nParam] = params_min;
+	    psTrace ("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+		     nParam, params[nParam], params_min);
+            return false;
+        }
+        return true;
+    case PS_MINIMIZE_PARAM_MAX:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            params_max =   1e5;
+            break;
+        case PM_PAR_I0:
+            params_max =   1e8;
+            break;
+        case PM_PAR_XPOS:
+            params_max =   1e4;
+            break;
+        case PM_PAR_YPOS:
+            params_max =   1e4;
+            break;
+        case PM_PAR_SXX:
+            params_max =   100;
+            break;
+        case PM_PAR_SYY:
+            params_max =   100;
+            break;
+        case PM_PAR_SXY:
+            params_max =  +q2;
+            break;
+        case PM_PAR_7:
+            params_max =  20.0;
+            break;
+        default:
+            psAbort("invalid parameter %d for param max test", nParam);
+        }
+        if (params[nParam] > params_max) {
+            params[nParam] = params_max;
+	    psTrace ("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+		     nParam, params[nParam], params_max);
+            return false;
+        }
+        return true;
+    default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+
+// make an initial guess for parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source)
+{
+    pmMoments *moments = source->moments;
+    pmPeak    *peak    = source->peak;
+    psF32     *PAR  = model->params->data.F32;
+
+    psEllipseMoments emoments;
+    emoments.x2 = moments->Sx;
+    emoments.y2 = moments->Sy;
+    emoments.xy = moments->Sxy;
+
+    // force the axis ratio to be < 20.0
+    psEllipseAxes axes = psEllipseMomentsToAxes (emoments, 20.0);
+
+    if (!isfinite(axes.major)) return false;
+    if (!isfinite(axes.minor)) return false;
+    if (!isfinite(axes.theta)) return false;
+
+    psEllipseShape shape = psEllipseAxesToShape (axes);
+
+    if (!isfinite(shape.sx))  return false;
+    if (!isfinite(shape.sy))  return false;
+    if (!isfinite(shape.sxy)) return false;
+
+    PAR[PM_PAR_SKY]  = moments->Sky;
+    PAR[PM_PAR_I0]   = moments->Peak - moments->Sky;
+    PAR[PM_PAR_XPOS] = peak->x;
+    PAR[PM_PAR_YPOS] = peak->y;
+    PAR[PM_PAR_SXX]  = PS_MAX(0.5, M_SQRT2*shape.sx);
+    PAR[PM_PAR_SYY]  = PS_MAX(0.5, M_SQRT2*shape.sy);
+    PAR[PM_PAR_SXY]  = shape.sxy;
+    PAR[PM_PAR_7]    = 1.0;
+
+    return(true);
+}
+
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    float z, norm;
+    psEllipseShape shape;
+
+    psF32 *PAR = params->data.F32;
+
+    shape.sx  = PAR[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = PAR[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    // Area is equivalent to 2 pi sigma^2
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 Area = 2.0 * M_PI * axes.major * axes.minor;
+
+    // the area needs to be multiplied by the integral of f(z)
+    norm = 0.0;
+
+    # define DZ 0.25
+
+    float f0 = 1.0;
+    float f1, f2;
+    for (z = DZ; z < 50; z += DZ) {
+        f1 = 1.0 / (1 + PAR[PM_PAR_7]*z + pow(z, 2.25));
+        z += DZ;
+        f2 = 1.0 / (1 + PAR[PM_PAR_7]*z + pow(z, 2.25));
+        norm += f0 + 4*f1 + f2;
+        f0 = f2;
+    }
+    norm *= DZ / 3.0;
+
+    psF64 Flux = PAR[PM_PAR_I0] * Area * norm;
+
+    return(Flux);
+}
+
+// define this function so it never returns Inf or NaN
+// return the radius which yields the requested flux
+psF64 PM_MODEL_RADIUS (const psVector *params, psF64 flux)
+{
+    psF64 z, f;
+    int Nstep = 0;
+    psEllipseShape shape;
+
+    psF32 *PAR = params->data.F32;
+
+    if (flux <= 0)
+        return (1.0);
+    if (PAR[PM_PAR_I0] <= 0)
+        return (1.0);
+    if (flux >= PAR[PM_PAR_I0])
+        return (1.0);
+
+    shape.sx  = PAR[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = PAR[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 sigma = axes.major;
+
+    psF64 limit = flux / PAR[PM_PAR_I0];
+
+    // use the fact that f is monotonically decreasing
+    z = 0;
+    Nstep = 0;
+
+    // choose a z value guaranteed to be beyond our limit
+    float z0 = pow((1.0 / limit), (1.0 / 2.25));
+    float z1 = (1.0 / limit) / PAR[PM_PAR_7];
+    z1 = PS_MAX (z0, z1);
+    z0 = 0.0;
+
+    // perform a type of bisection to find the value
+    float f0 = 1.0 / (1 + PAR[PM_PAR_7]*z0 + pow(z0, 2.25));
+    float f1 = 1.0 / (1 + PAR[PM_PAR_7]*z1 + pow(z1, 2.25));
+    while ((Nstep < 10) && (fabs(z1 - z0) > 0.5)) {
+        z = 0.5*(z0 + z1);
+        f = 1.0 / (1 + PAR[PM_PAR_7]*z + pow(z, 2.25));
+        if (f > limit) {
+            z0 = z;
+            f0 = f;
+        } else {
+            z1 = z;
+            f1 = f;
+        }
+        Nstep ++;
+    }
+    psF64 radius = sigma * sqrt (2.0 * z);
+
+    if (isnan(radius))
+        psAbort("error in code: radius is NaN");
+
+    return (radius);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, pmPSF *psf)
+{
+
+    psF32 *out = modelPSF->params->data.F32;
+    psF32 *in  = modelFLT->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    for (int i = 0; i < psf->params->n; i++) {
+        if (psf->params->data[i] == NULL) {
+            out[i] = in[i];
+        } else {
+            pmTrend2D *trend = psf->params->data[i];
+            out[i] = pmTrend2DEval(trend, in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
+        }
+    }
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    if (!pmPSF_FitToModel (out, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)",
+		in[PM_PAR_YPOS], in[PM_PAR_XPOS]);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS(PS_MINIMIZE_PARAM_MIN, i, out, NULL);
+	status &= PM_MODEL_LIMITS(PS_MINIMIZE_PARAM_MAX, i, out, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)",
+		     in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
+	    modelPSF->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+
+    return true;
+}
+
+// construct the PSF model from the FLT model and the psf
+// XXX is this sufficiently general do be a global function, not a pmModelClass function?
+bool PM_MODEL_PARAMS_FROM_PSF (pmModel *model, pmPSF *psf, float Xo, float Yo, float Io)
+{
+    psF32 *PAR = model->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    PAR[PM_PAR_SKY]  = 0.0;
+    PAR[PM_PAR_I0]   = Io;
+    PAR[PM_PAR_XPOS] = Xo;
+    PAR[PM_PAR_YPOS] = Yo;
+    
+    // supply the model-fitted parameters, or copy from the input
+    for (int i = 0; i < psf->params->n; i++) {
+	if (i == PM_PAR_SKY) continue;
+	if (i == PM_PAR_I0) continue;
+	if (i == PM_PAR_XPOS) continue;
+	if (i == PM_PAR_YPOS) continue;
+	pmTrend2D *trend = psf->params->data[i];
+	PAR[i] = pmTrend2DEval(trend, Xo, Yo);
+    }
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    // XXX user-defined value for limit?
+    if (!pmPSF_FitToModel (PAR, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)", Xo, Yo);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MIN, i, PAR, NULL);
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MAX, i, PAR, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)", Xo, Yo);
+	    model->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+    return(true);
+}
+
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+
+    psF32 dP;
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    dP = 0;
+    dP += PS_SQR(dPAR[PM_PAR_SXX] / PAR[PM_PAR_SXX]);
+    dP += PS_SQR(dPAR[PM_PAR_SYY] / PAR[PM_PAR_SYY]);
+    dP = sqrt (dP);
+
+    status = true;
+    status &= (dP < 0.5);
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+# undef PM_MODEL_FUNC
+# undef PM_MODEL_FLUX
+# undef PM_MODEL_GUESS
+# undef PM_MODEL_LIMITS
+# undef PM_MODEL_RADIUS
+# undef PM_MODEL_FROM_PSF
+# undef PM_MODEL_PARAMS_FROM_PSF
+# undef PM_MODEL_FIT_STATUS
Index: /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_RGAUSS.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_RGAUSS.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_RGAUSS.c	(revision 15358)
@@ -0,0 +1,466 @@
+/******************************************************************************
+ * this file defines the RGAUSS source shape model (XXX need a better name!).  Note that these
+ * model functions are loaded by pmModelGroup.c using 'include', and thus need no 'include'
+ * statements of their own.  The models use a psVector to represent the set of parameters, with
+ * the sequence used to specify the meaning of the parameter.  The meaning of the parameters
+ * may thus vary depending on the specifics of the model.  All models which are used a PSF
+ * representations share a few parameters, for which # define names are listed in pmModel.h:
+ 
+   power-law with fitted slope
+   1 / (1 + z + z^alpha)
+ 
+ * PM_PAR_SKY 0   - local sky : note that this is unused and may be dropped in the future
+ * PM_PAR_I0 1    - central intensity
+ * PM_PAR_XPOS 2  - X center of object
+ * PM_PAR_YPOS 3  - Y center of object
+ * PM_PAR_SXX 4   - X^2 term of elliptical contour (sqrt(2) / SigmaX)
+ * PM_PAR_SYY 5   - Y^2 term of elliptical contour (sqrt(2) / SigmaY)
+ * PM_PAR_SXY 6   - X*Y term of elliptical contour
+ * PM_PAR_7   7   - power-law slope (alpha)
+ *****************************************************************************/
+
+# define PM_MODEL_FUNC       	  pmModelFunc_RGAUSS
+# define PM_MODEL_FLUX       	  pmModelFlux_RGAUSS
+# define PM_MODEL_GUESS      	  pmModelGuess_RGAUSS
+# define PM_MODEL_LIMITS     	  pmModelLimits_RGAUSS
+# define PM_MODEL_RADIUS     	  pmModelRadius_RGAUSS
+# define PM_MODEL_FROM_PSF   	  pmModelFromPSF_RGAUSS
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_RGAUSS
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_RGAUSS
+
+psF32 PM_MODEL_FUNC (psVector *deriv,
+                     const psVector *params,
+                     const psVector *pixcoord)
+{
+    psF32 *PAR = params->data.F32;
+
+    psF32 X  = pixcoord->data.F32[0] - PAR[PM_PAR_XPOS];
+    psF32 Y  = pixcoord->data.F32[1] - PAR[PM_PAR_YPOS];
+    psF32 px = X / PAR[PM_PAR_SXX];
+    psF32 py = Y / PAR[PM_PAR_SYY];
+    psF32 z  = PS_SQR(px) + PS_SQR(py) + X*Y*PAR[PM_PAR_SXY];
+
+    assert (z >= 0);
+
+    psF32 p  = pow(z, PAR[PM_PAR_7] - 1.0);
+    psF32 r  = 1.0 / (1 + z + z*p);
+    psF32 f  = PAR[PM_PAR_I0]*r + PAR[PM_PAR_SKY];
+
+    if (deriv != NULL) {
+        psF32 *dPAR = deriv->data.F32;
+
+        // note difference from a pure gaussian: q = params->data.F32[PM_PAR_I0]*r
+        psF32 t = PAR[PM_PAR_I0]*r*r;
+        psF32 q = t*(1 + PAR[PM_PAR_7]*p);
+
+        dPAR[PM_PAR_SKY] = +1.0;
+        dPAR[PM_PAR_I0] = +r;
+        dPAR[PM_PAR_XPOS] = q*(2.0*px/PAR[PM_PAR_SXX] + Y*PAR[PM_PAR_SXY]);
+        dPAR[PM_PAR_YPOS] = q*(2.0*py/PAR[PM_PAR_SYY] + X*PAR[PM_PAR_SXY]);
+        dPAR[PM_PAR_SXX] = +2.0*q*px*px/PAR[PM_PAR_SXX];
+        dPAR[PM_PAR_SYY] = +2.0*q*py*py/PAR[PM_PAR_SYY];
+        dPAR[PM_PAR_SXY] = -q*X*Y;
+
+	// this model derivative is undefined at z = 0.0, but is actually 0.0
+        dPAR[PM_PAR_7] = (z == 0.0) ? 0.0 : -5.0*t*log(z)*p*z;
+    }
+    return(f);
+}
+
+// define the parameter limits
+// AR_MAX is the maximum allowed axis ratio
+// AR_RATIO is ((1-R)/(1+R))^2 where R = AR_MAX^(-2)
+# define AR_MAX 20.0
+# define AR_RATIO 0.99
+bool PM_MODEL_LIMITS (psMinConstraintMode mode, int nParam, float *params, float *beta)
+{
+    float beta_lim = 0, params_min = 0, params_max = 0;
+    float f1 = 0, f2 = 0, q1 = 0, q2 = 0;
+
+    // we need to calculate the limits for SXY specially
+    if (nParam == PM_PAR_SXY) {
+        f1 = 1.0 / PS_SQR(params[PM_PAR_SYY]) + 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        f2 = 1.0 / PS_SQR(params[PM_PAR_SYY]) - 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        q1 = PS_SQR(f1)*AR_RATIO - PS_SQR(f2);
+	q1 = (q1 < 0.0) ? 0.0 : q1;
+	// if q1 < 0.0, f2 ~ f1, we have a very large axis ratio near 45deg..  Saturate at that
+	// angle and let f2,f1 fight it out
+        q2  = 0.5*sqrt (q1);
+    }
+
+    switch (mode) {
+    case PS_MINIMIZE_BETA_LIMIT:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            beta_lim = 1000;
+            break;
+        case PM_PAR_I0:
+            beta_lim = 3e6;
+            break;
+        case PM_PAR_XPOS:
+            beta_lim = 5;
+            break;
+        case PM_PAR_YPOS:
+            beta_lim = 5;
+            break;
+        case PM_PAR_SXX:
+            beta_lim = 0.5;
+            break;
+        case PM_PAR_SYY:
+            beta_lim = 0.5;
+            break;
+        case PM_PAR_SXY:
+            beta_lim =  0.5*q2;
+            break;
+        case PM_PAR_7:
+            beta_lim = 0.5;
+            break;
+        default:
+            psAbort("invalid parameter %d for beta test", nParam);
+        }
+        if (fabs(beta[nParam]) > fabs(beta_lim)) {
+            beta[nParam] = (beta[nParam] > 0) ? fabs(beta_lim) : -fabs(beta_lim);
+	    psTrace ("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+		     nParam, beta[nParam], beta_lim);
+            return false;
+        }
+        return true;
+    case PS_MINIMIZE_PARAM_MIN:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            params_min = -1000;
+            break;
+        case PM_PAR_I0:
+            params_min =   0.01;
+            break;
+        case PM_PAR_XPOS:
+            params_min =  -100;
+            break;
+        case PM_PAR_YPOS:
+            params_min =  -100;
+            break;
+        case PM_PAR_SXX:
+            params_min =   0.5;
+            break;
+        case PM_PAR_SYY:
+            params_min =   0.5;
+            break;
+        case PM_PAR_SXY:
+            params_min =  -q2;
+            break;
+        case PM_PAR_7:
+            params_min =   1.25;
+            break;
+        default:
+            psAbort("invalid parameter %d for param min test", nParam);
+        }
+        if (params[nParam] < params_min) {
+            params[nParam] = params_min;
+	    psTrace ("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+		     nParam, params[nParam], params_min);
+            return false;
+        }
+        return true;
+    case PS_MINIMIZE_PARAM_MAX:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            params_max =   1e5;
+            break;
+        case PM_PAR_I0:
+            params_max =   1e8;
+            break;
+        case PM_PAR_XPOS:
+            params_max =   1e4;
+            break;
+        case PM_PAR_YPOS:
+            params_max =   1e4;
+            break;
+        case PM_PAR_SXX:
+            params_max =   100;
+            break;
+        case PM_PAR_SYY:
+            params_max =   100;
+            break;
+        case PM_PAR_SXY:
+            params_max =  +q2;
+            break;
+        case PM_PAR_7:
+            params_max =  4.0;
+            break;
+        default:
+            psAbort("invalid parameter %d for param max test", nParam);
+        }
+        if (params[nParam] > params_max) {
+            params[nParam] = params_max;
+	    psTrace ("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+		     nParam, params[nParam], params_max);
+            return false;
+        }
+        return true;
+    default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+// make an initial guess for parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source)
+{
+    pmMoments *moments = source->moments;
+    pmPeak    *peak    = source->peak;
+    psF32     *PAR  = model->params->data.F32;
+
+    psEllipseMoments emoments;
+    emoments.x2 = moments->Sx;
+    emoments.y2 = moments->Sy;
+    emoments.xy = moments->Sxy;
+
+    // force the axis ratio to be < 20.0
+    psEllipseAxes axes = psEllipseMomentsToAxes (emoments, 20.0);
+
+    if (!isfinite(axes.major)) return false;
+    if (!isfinite(axes.minor)) return false;
+    if (!isfinite(axes.theta)) return false;
+
+    psEllipseShape shape = psEllipseAxesToShape (axes);
+
+    if (!isfinite(shape.sx))  return false;
+    if (!isfinite(shape.sy))  return false;
+    if (!isfinite(shape.sxy)) return false;
+
+    PAR[PM_PAR_SKY]  = moments->Sky;
+    PAR[PM_PAR_I0]   = moments->Peak - moments->Sky;
+    PAR[PM_PAR_XPOS] = peak->x;
+    PAR[PM_PAR_YPOS] = peak->y;
+    PAR[PM_PAR_SXX]  = PS_MAX(0.5, M_SQRT2*shape.sx);
+    PAR[PM_PAR_SYY]  = PS_MAX(0.5, M_SQRT2*shape.sy);
+    PAR[PM_PAR_SXY]  = shape.sxy;
+    PAR[PM_PAR_7]    = 2.25;
+
+    return(true);
+}
+
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    float norm, z;
+    psEllipseShape shape;
+
+    psF32 *PAR = params->data.F32;
+
+    shape.sx  = PAR[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = PAR[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    // Area is equivalent to 2 pi sigma^2
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 Area = 2.0 * M_PI * axes.major * axes.minor;
+
+    // the area needs to be multiplied by the integral of f(z)
+    norm = 0.0;
+
+    # define DZ 0.25
+
+    float f0 = 1.0;
+    float f1, f2;
+    for (z = DZ; z < 50; z += DZ) {
+        f1 = 1.0 / (1 + z + pow(z, PAR[PM_PAR_7]));
+        z += DZ;
+        f2 = 1.0 / (1 + z + pow(z, PAR[PM_PAR_7]));
+        norm += f0 + 4*f1 + f2;
+        f0 = f2;
+    }
+    norm *= DZ / 3.0;
+
+    psF64 Flux = PAR[PM_PAR_I0] * Area * norm;
+
+    return(Flux);
+}
+
+// define this function so it never returns Inf or NaN
+// return the radius which yields the requested flux
+psF64 PM_MODEL_RADIUS (const psVector *params, psF64 flux)
+{
+    psF64 z, f;
+    int Nstep = 0;
+    psEllipseShape shape;
+
+    psF32 *PAR = params->data.F32;
+
+    if (flux <= 0)
+        return (1.0);
+    if (PAR[PM_PAR_I0] <= 0)
+        return (1.0);
+    if (flux >= PAR[PM_PAR_I0])
+        return (1.0);
+
+    shape.sx  = PAR[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = PAR[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 sigma = axes.major;
+
+    psF64 limit = flux / PAR[PM_PAR_I0];
+
+    // use the fact that f is monotonically decreasing
+    z = 0;
+    Nstep = 0;
+
+    // choose a z value guaranteed to be beyond our limit
+    float z0 = pow((1.0 / limit), (1.0 / PAR[PM_PAR_7]));
+    float z1 = (1.0 / limit);
+    z1 = PS_MAX (z0, z1);
+    z0 = 0.0;
+
+    // perform a type of bisection to find the value
+    float f0 = 1.0 / (1 + z0 + pow(z0, PAR[PM_PAR_7]));
+    float f1 = 1.0 / (1 + z1 + pow(z1, PAR[PM_PAR_7]));
+    while ((Nstep < 10) && (fabs(z1 - z0) > 0.5)) {
+        z = 0.5*(z0 + z1);
+        f = 1.0 / (1 + z + pow(z, PAR[PM_PAR_7]));
+        if (f > limit) {
+            z0 = z;
+            f0 = f;
+        } else {
+            z1 = z;
+            f1 = f;
+        }
+        Nstep ++;
+    }
+    psF64 radius = sigma * sqrt (2.0 * z);
+
+    if (isnan(radius))
+        psAbort("error in code: radius is NaN");
+
+    return (radius);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, pmPSF *psf)
+{
+
+    psF32 *out = modelPSF->params->data.F32;
+    psF32 *in  = modelFLT->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    for (int i = 0; i < psf->params->n; i++) {
+        if (psf->params->data[i] == NULL) {
+            out[i] = in[i];
+        } else {
+            pmTrend2D *trend = psf->params->data[i];
+            out[i] = pmTrend2DEval(trend, in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
+        }
+    }
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    if (!pmPSF_FitToModel (out, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)",
+		in[PM_PAR_YPOS], in[PM_PAR_XPOS]);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS(PS_MINIMIZE_PARAM_MIN, i, out, NULL);
+	status &= PM_MODEL_LIMITS(PS_MINIMIZE_PARAM_MAX, i, out, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)",
+		     in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
+	    modelPSF->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+
+    return true;
+}
+
+// construct the PSF model from the FLT model and the psf
+// XXX is this sufficiently general do be a global function, not a pmModelClass function?
+bool PM_MODEL_PARAMS_FROM_PSF (pmModel *model, pmPSF *psf, float Xo, float Yo, float Io)
+{
+    psF32 *PAR = model->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    PAR[PM_PAR_SKY]  = 0.0;
+    PAR[PM_PAR_I0]   = Io;
+    PAR[PM_PAR_XPOS] = Xo;
+    PAR[PM_PAR_YPOS] = Yo;
+    
+    // supply the model-fitted parameters, or copy from the input
+    for (int i = 0; i < psf->params->n; i++) {
+	if (i == PM_PAR_SKY) continue;
+	if (i == PM_PAR_I0) continue;
+	if (i == PM_PAR_XPOS) continue;
+	if (i == PM_PAR_YPOS) continue;
+	pmTrend2D *trend = psf->params->data[i];
+	PAR[i] = pmTrend2DEval(trend, Xo, Yo);
+    }
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    // XXX user-defined value for limit?
+    if (!pmPSF_FitToModel (PAR, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)", Xo, Yo);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MIN, i, PAR, NULL);
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MAX, i, PAR, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)", Xo, Yo);
+	    model->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+    return(true);
+}
+
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+
+    psF32 dP;
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    dP = 0;
+    dP += PS_SQR(dPAR[PM_PAR_SXX] / PAR[PM_PAR_SXX]);
+    dP += PS_SQR(dPAR[PM_PAR_SYY] / PAR[PM_PAR_SYY]);
+    dP = sqrt (dP);
+
+    status = true;
+    status &= (dP < 0.5);
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+# undef PM_MODEL_FUNC
+# undef PM_MODEL_FLUX
+# undef PM_MODEL_GUESS
+# undef PM_MODEL_LIMITS
+# undef PM_MODEL_RADIUS
+# undef PM_MODEL_FROM_PSF
+# undef PM_MODEL_PARAMS_FROM_PSF
+# undef PM_MODEL_FIT_STATUS
Index: /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_SERSIC.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_SERSIC.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_SERSIC.c	(revision 15358)
@@ -0,0 +1,458 @@
+/******************************************************************************
+ * this file defines the SERSIC source shape model.  Note that these model functions are loaded
+ * by pmModelGroup.c using 'include', and thus need no 'include' statements of their own.  The
+ * models use a psVector to represent the set of parameters, with the sequence used to specify
+ * the meaning of the parameter.  The meaning of the parameters may thus vary depending on the
+ * specifics of the model.  All models which are used a PSF representations share a few
+ * parameters, for which # define names are listed in pmModel.h:
+ 
+   f = exp(-z^n)
+ 
+   * PM_PAR_SKY 0   - local sky : note that this is unused and may be dropped in the future
+   * PM_PAR_I0 1    - central intensity
+   * PM_PAR_XPOS 2  - X center of object
+   * PM_PAR_YPOS 3  - Y center of object
+   * PM_PAR_SXX 4   - X^2 term of elliptical contour (sqrt(2) / SigmaX)
+   * PM_PAR_SYY 5   - Y^2 term of elliptical contour (sqrt(2) / SigmaY)
+   * PM_PAR_SXY 6   - X*Y term of elliptical contour
+   * PM_PAR_7   7   - normalized sersic parameter
+
+   note that a standard sersic model uses exp(-K*(z^(1/n) - 1).  the additional elements (K,
+   the -1 offset) are absorbed in this model by the normalization, the exponenet, and the
+   radial scale.  We fit the elements in this form, then re-normalize them on output.
+   *****************************************************************************/
+
+# define PM_MODEL_FUNC       	  pmModelFunc_SERSIC
+# define PM_MODEL_FLUX       	  pmModelFlux_SERSIC
+# define PM_MODEL_GUESS      	  pmModelGuess_SERSIC
+# define PM_MODEL_LIMITS     	  pmModelLimits_SERSIC
+# define PM_MODEL_RADIUS     	  pmModelRadius_SERSIC
+# define PM_MODEL_FROM_PSF   	  pmModelFromPSF_SERSIC
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_SERSIC
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_SERSIC
+
+psF32 PM_MODEL_FUNC (psVector *deriv,
+                     const psVector *params,
+                     const psVector *pixcoord)
+{
+    psF32 *PAR = params->data.F32;
+
+    psF32 X  = pixcoord->data.F32[0] - PAR[PM_PAR_XPOS];
+    psF32 Y  = pixcoord->data.F32[1] - PAR[PM_PAR_YPOS];
+    psF32 px = X / PAR[PM_PAR_SXX];
+    psF32 py = Y / PAR[PM_PAR_SYY];
+    psF32 z  = PS_SQR(px) + PS_SQR(py) + PAR[PM_PAR_SXY]*X*Y;
+
+    // XXX if the elliptical contour is defined in valid way, this step should not be required.
+    // other models (like PGAUSS) don't use fractional powers, and thus do not have NaN values
+    // for negative values of z
+    // XXX use an assert here to force the elliptical parameters to be correctly determined
+    // if (z < 0) z = 0;
+    assert (z >= 0);
+
+    psF32 f2 = pow(z,PAR[PM_PAR_7]);
+    psF32 f1 = exp(-f2);
+    psF32 z0 = PAR[PM_PAR_I0]*f1;
+    psF32 f0 = PAR[PM_PAR_SKY] + z0;
+
+    assert (isfinite(f2));
+    assert (isfinite(f1));
+    assert (isfinite(z0));
+    assert (isfinite(f0));
+
+    if (deriv != NULL) {
+        psF32 *dPAR = deriv->data.F32;
+
+	// gradient is infinite for z = 0; saturate at z = 0.01
+	psF32 z1 = (z < 0.01) ? z0*PAR[PM_PAR_7]*pow(0.01,PAR[PM_PAR_7] - 1.0) : z0*PAR[PM_PAR_7]*pow(z,PAR[PM_PAR_7] - 1.0);
+
+        dPAR[PM_PAR_SKY]  = +1.0;
+        dPAR[PM_PAR_I0]   = +f1;
+        dPAR[PM_PAR_7]    = (z == 0.0) ? 0.0 : -z0*f2*log(z);
+
+	assert (isfinite(z1));
+	assert (isfinite(dPAR[PM_PAR_7]));
+
+        dPAR[PM_PAR_XPOS] = +1.0*z1*(2.0*px/PAR[PM_PAR_SXX] + Y*PAR[PM_PAR_SXY]);
+        dPAR[PM_PAR_YPOS] = +1.0*z1*(2.0*py/PAR[PM_PAR_SYY] + X*PAR[PM_PAR_SXY]);
+        dPAR[PM_PAR_SXX]  = +2.0*z1*px*px/PAR[PM_PAR_SXX];
+        dPAR[PM_PAR_SYY]  = +2.0*z1*py*py/PAR[PM_PAR_SYY];
+        dPAR[PM_PAR_SXY]  = -1.0*z1*X*Y;
+        dPAR[PM_PAR_SXY]  = -1.0*z1*X*Y;
+    }
+    return (f0);
+}
+
+// define the parameter limits
+// AR_MAX is the maximum allowed axis ratio
+// AR_RATIO is ((1-R)/(1+R))^2 where R = AR_MAX^(-2)
+# define AR_MAX 20.0
+# define AR_RATIO 0.99
+bool PM_MODEL_LIMITS (psMinConstraintMode mode, int nParam, float *params, float *beta)
+{
+    float beta_lim = 0, params_min = 0, params_max = 0;
+    float f1 = 0, f2 = 0, q1 = 0, q2 = 0;
+
+    // we need to calculate the limits for SXY specially
+    if (nParam == PM_PAR_SXY) {
+        f1 = 1.0 / PS_SQR(params[PM_PAR_SYY]) + 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        f2 = 1.0 / PS_SQR(params[PM_PAR_SYY]) - 1.0 / PS_SQR(params[PM_PAR_SXX]);
+        q1 = PS_SQR(f1)*AR_RATIO - PS_SQR(f2);
+	q1 = (q1 < 0.0) ? 0.0 : q1;
+	// if q1 < 0.0, f2 ~ f1, we have a very large axis ratio near 45deg..  Saturate at that
+	// angle and let f2,f1 fight it out
+        q2  = 0.5*sqrt (q1);
+    }
+
+    switch (mode) {
+    case PS_MINIMIZE_BETA_LIMIT:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            beta_lim = 1000;
+            break;
+        case PM_PAR_I0:
+            beta_lim = 3e6;
+            break;
+        case PM_PAR_XPOS:
+            beta_lim = 5;
+            break;
+        case PM_PAR_YPOS:
+            beta_lim = 5;
+            break;
+        case PM_PAR_SXX:
+            beta_lim = 1.0;
+            break;
+        case PM_PAR_SYY:
+            beta_lim = 1.0;
+            break;
+        case PM_PAR_SXY:
+            beta_lim =  0.5*q2;
+            break;
+        case PM_PAR_7:
+            beta_lim = 2.0;
+            break;
+        default:
+            psAbort("invalid parameter %d for beta test", nParam);
+        }
+        if (fabs(beta[nParam]) > fabs(beta_lim)) {
+            beta[nParam] = (beta[nParam] > 0) ? fabs(beta_lim) : -fabs(beta_lim);
+	    psTrace ("psModules.objects", 5, "|beta[nParam==%d]| > |beta_lim|; %g v. %g",
+		     nParam, beta[nParam], beta_lim);
+            return false;
+        }
+        return true;
+    case PS_MINIMIZE_PARAM_MIN:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            params_min = -1000;
+            break;
+        case PM_PAR_I0:
+            params_min =     0.01;
+            break;
+        case PM_PAR_XPOS:
+            params_min =  -100;
+            break;
+        case PM_PAR_YPOS:
+            params_min =  -100;
+            break;
+        case PM_PAR_SXX:
+            params_min =   0.25;
+            break;
+        case PM_PAR_SYY:
+            params_min =   0.25;
+            break;
+        case PM_PAR_SXY:
+            params_min =  -q2;
+            break;
+        case PM_PAR_7:
+            params_min =   0.1;
+            break;
+        default:
+            psAbort("invalid parameter %d for param min test", nParam);
+        }
+        if (params[nParam] < params_min) {
+            params[nParam] = params_min;
+	    psTrace ("psModules.objects", 5, "params[nParam==%d] < params_min; %g v. %g",
+		     nParam, params[nParam], params_min);
+            return false;
+        }
+        return true;
+    case PS_MINIMIZE_PARAM_MAX:
+        switch (nParam) {
+        case PM_PAR_SKY:
+            params_max =   1e5;
+            break;
+        case PM_PAR_I0:
+            params_max =   1e8;
+            break;
+        case PM_PAR_XPOS:
+            params_max =   1e4;
+            break;
+        case PM_PAR_YPOS:
+            params_max =   1e4;
+            break;
+        case PM_PAR_SXX:
+            params_max =   100;
+            break;
+        case PM_PAR_SYY:
+            params_max =   100;
+            break;
+        case PM_PAR_SXY:
+            params_max =  +q2;
+            break;
+        case PM_PAR_7:
+            params_max =   4.0;
+            break;
+        default:
+            psAbort("invalid parameter %d for param max test", nParam);
+        }
+        if (params[nParam] > params_max) {
+            params[nParam] = params_max;
+	    psTrace ("psModules.objects", 5, "params[nParam==%d] > params_max; %g v. %g",
+		     nParam, params[nParam], params_max);
+            return false;
+        }
+        return true;
+    default:
+        psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+
+// make an initial guess for parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source)
+{
+    pmMoments *moments = source->moments;
+    pmPeak    *peak    = source->peak;
+    psF32     *PAR  = model->params->data.F32;
+
+    psEllipseMoments emoments;
+    emoments.x2 = moments->Sx;
+    emoments.y2 = moments->Sy;
+    emoments.xy = moments->Sxy;
+
+    // force the axis ratio to be < 20.0
+    psEllipseAxes axes = psEllipseMomentsToAxes (emoments, 20.0);
+
+    if (!isfinite(axes.major)) return false;
+    if (!isfinite(axes.minor)) return false;
+    if (!isfinite(axes.theta)) return false;
+
+    psEllipseShape shape = psEllipseAxesToShape (axes);
+
+    if (!isfinite(shape.sx))  return false;
+    if (!isfinite(shape.sy))  return false;
+    if (!isfinite(shape.sxy)) return false;
+
+    PAR[PM_PAR_SKY]  = moments->Sky;
+    PAR[PM_PAR_I0]   = moments->Peak - moments->Sky;
+    PAR[PM_PAR_XPOS] = peak->x;
+    PAR[PM_PAR_YPOS] = peak->y;
+    PAR[PM_PAR_SXX]  = PS_MAX(0.5, M_SQRT2*shape.sx);
+    PAR[PM_PAR_SYY]  = PS_MAX(0.5, M_SQRT2*shape.sy);
+    PAR[PM_PAR_SXY]  = shape.sxy;
+    PAR[PM_PAR_7]    = 0.5;
+
+    return(true);
+}
+
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    float z, norm;
+    psEllipseShape shape;
+
+    psF32 *PAR = params->data.F32;
+
+    shape.sx  = PAR[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = PAR[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    // Area is equivalent to 2 pi sigma^2
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 Area = 2.0 * M_PI * axes.major * axes.minor;
+
+    // the area needs to be multiplied by the integral of f(z)
+    norm = 0.0;
+
+    # define DZ 0.25
+
+    float f0 = 1.0;
+    float f1, f2;
+    for (z = DZ; z < 50; z += DZ) {
+        f1 = 1.0 / (1 + PAR[PM_PAR_7]*z + pow(z, 2.25));
+        z += DZ;
+        f2 = 1.0 / (1 + PAR[PM_PAR_7]*z + pow(z, 2.25));
+        norm += f0 + 4*f1 + f2;
+        f0 = f2;
+    }
+    norm *= DZ / 3.0;
+
+    psF64 Flux = PAR[PM_PAR_I0] * Area * norm;
+
+    return(Flux);
+}
+
+// define this function so it never returns Inf or NaN
+// return the radius which yields the requested flux
+psF64 PM_MODEL_RADIUS (const psVector *params, psF64 flux)
+{
+    psEllipseShape shape;
+
+    psF32 *PAR = params->data.F32;
+
+    if (flux <= 0)
+        return (1.0);
+    if (PAR[PM_PAR_I0] <= 0)
+        return (1.0);
+    if (flux >= PAR[PM_PAR_I0])
+        return (1.0);
+
+    shape.sx  = PAR[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = PAR[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 sigma = axes.major;
+
+    psF64 limit = flux / PAR[PM_PAR_I0];
+
+    psF64 z = pow (-log(limit), (1.0 / PAR[PM_PAR_7]));
+
+    psF64 radius = sigma * sqrt (2.0 * z);
+
+    if (isnan(radius))
+        psAbort("error in code: radius is NaN");
+
+    return (radius);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, pmPSF *psf)
+{
+
+    psF32 *out = modelPSF->params->data.F32;
+    psF32 *in  = modelFLT->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    for (int i = 0; i < psf->params->n; i++) {
+        if (psf->params->data[i] == NULL) {
+            out[i] = in[i];
+        } else {
+            pmTrend2D *trend = psf->params->data[i];
+            out[i] = pmTrend2DEval(trend, in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
+        }
+    }
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    if (!pmPSF_FitToModel (out, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)",
+		in[PM_PAR_YPOS], in[PM_PAR_XPOS]);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS(PS_MINIMIZE_PARAM_MIN, i, out, NULL);
+	status &= PM_MODEL_LIMITS(PS_MINIMIZE_PARAM_MAX, i, out, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)",
+		     in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
+	    modelPSF->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+
+    return true;
+}
+
+// construct the PSF model from the FLT model and the psf
+// XXX is this sufficiently general do be a global function, not a pmModelClass function?
+bool PM_MODEL_PARAMS_FROM_PSF (pmModel *model, pmPSF *psf, float Xo, float Yo, float Io)
+{
+    psF32 *PAR = model->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    PAR[PM_PAR_SKY]  = 0.0;
+    PAR[PM_PAR_I0]   = Io;
+    PAR[PM_PAR_XPOS] = Xo;
+    PAR[PM_PAR_YPOS] = Yo;
+    
+    // supply the model-fitted parameters, or copy from the input
+    for (int i = 0; i < psf->params->n; i++) {
+	if (i == PM_PAR_SKY) continue;
+	if (i == PM_PAR_I0) continue;
+	if (i == PM_PAR_XPOS) continue;
+	if (i == PM_PAR_YPOS) continue;
+	pmTrend2D *trend = psf->params->data[i];
+	PAR[i] = pmTrend2DEval(trend, Xo, Yo);
+    }
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    // XXX user-defined value for limit?
+    if (!pmPSF_FitToModel (PAR, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)", Xo, Yo);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MIN, i, PAR, NULL);
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MAX, i, PAR, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)", Xo, Yo);
+	    model->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+    return(true);
+}
+
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+
+    psF32 dP;
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    dP = 0;
+    dP += PS_SQR(dPAR[PM_PAR_SXX] / PAR[PM_PAR_SXX]);
+    dP += PS_SQR(dPAR[PM_PAR_SYY] / PAR[PM_PAR_SYY]);
+    dP = sqrt (dP);
+
+    status = true;
+    status &= (dP < 0.5);
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+# undef PM_MODEL_FUNC
+# undef PM_MODEL_FLUX
+# undef PM_MODEL_GUESS
+# undef PM_MODEL_LIMITS
+# undef PM_MODEL_RADIUS
+# undef PM_MODEL_FROM_PSF
+# undef PM_MODEL_PARAMS_FROM_PSF
+# undef PM_MODEL_FIT_STATUS
Index: /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_SGAUSS.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_SGAUSS.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_SGAUSS.c	(revision 15358)
@@ -0,0 +1,386 @@
+/******************************************************************************
+ * this file defines the SGAUSS source shape model (XXX need a better name!).  Note that these
+ * model functions are loaded by pmModelGroup.c using 'include', and thus need no 'include'
+ * statements of their own.  The models use a psVector to represent the set of parameters, with
+ * the sequence used to specify the meaning of the parameter.  The meaning of the parameters
+ * may thus vary depending on the specifics of the model.  All models which are used a PSF
+ * representations share a few parameters, for which # define names are listed in pmModel.h:
+ 
+   power-law with fitted slope and outer tidal radius
+   1 / (1 + z^N + kz^4)
+ 
+   * PM_PAR_SKY 0   - local sky : note that this is unused and may be dropped in the future
+   * PM_PAR_I0 1    - central intensity
+   * PM_PAR_XPOS 2  - X center of object
+   * PM_PAR_YPOS 3  - Y center of object
+   * PM_PAR_SXX 4   - X^2 term of elliptical contour (sqrt(2) / SigmaX)
+   * PM_PAR_SYY 5   - Y^2 term of elliptical contour (sqrt(2) / SigmaY)
+   * PM_PAR_SXY 6   - X*Y term of elliptical contour
+   * PM_PAR_7   7   - slope of power-law component (N)
+   * PM_PAR_8   8   - amplitude of the tidal cutoff (k)
+   *****************************************************************************/
+
+/***
+    XXXX the model in this file needs to be tested more carefully.
+    the code for guessing the power-law slope based on the radial profile 
+    is either too slow or does not work well.
+    fix up the code to follow conventions in the other model function files.
+***/
+
+XXX broken code
+
+# define PM_MODEL_FUNC       pmModelFunc_SGAUSS
+# define PM_MODEL_FLUX       pmModelFlux_SGAUSS
+# define PM_MODEL_GUESS      pmModelGuess_SGAUSS
+# define PM_MODEL_LIMITS     pmModelLimits_SGAUSS
+# define PM_MODEL_RADIUS     pmModelRadius_SGAUSS
+# define PM_MODEL_FROM_PSF   pmModelFromPSF_SGAUSS
+# define PM_MODEL_FIT_STATUS pmModelFitStatus_SGAUSS
+
+psF64 psImageEllipseContour (psEllipseAxes axes, double xc, double yc, psImage *image);
+
+psF32 PM_MODEL_FUNC (psVector *deriv,
+                     const psVector *params,
+                     const psVector *x)
+{
+    psF32 *PAR = params->data.F32;
+
+    psF32 X  = x->data.F32[0] - PAR[2];
+    psF32 Y  = x->data.F32[1] - PAR[3];
+    psF32 px = PAR[4]*X;
+    psF32 py = PAR[5]*Y;
+    psF32 z  = PS_MAX((0.5*PS_SQR(px) + 0.5*PS_SQR(py) + PAR[6]*X*Y), 1e-8);
+    // note that if z -> 0, dPAR[7] -> -inf
+    // also z^(PAR[7]-1) -> Inf
+
+    psF32 pr = z*PAR[8];
+    psF32 pr3 = pr*pr*pr;
+    psF32 p  = pow(z, PAR[7] - 1.0);
+    psF32 r  = 1.0 / (1 + z*p + pr*pr3);
+    psF32 f  = PAR[1]*r + PAR[0];
+
+    if (deriv != NULL) {
+        // note difference from a pure gaussian: q = params->data.F32[1]*r
+        psF32 t = PAR[1]*r*r;
+        psF32 q = t*(PAR[7]*p + 4*PAR[8]*pr3);
+
+        deriv->data.F32[0] = +1.0;
+        deriv->data.F32[1] = +r;
+        deriv->data.F32[2] = q*(2.0*px*PAR[4] + PAR[6]*Y);
+        deriv->data.F32[3] = q*(2.0*py*PAR[5] + PAR[6]*X);
+        deriv->data.F32[4] = -2.0*q*px*X;
+        deriv->data.F32[5] = -2.0*q*py*Y;
+        deriv->data.F32[6] = -q*X*Y;
+        deriv->data.F32[7] = -2*t*log(z)*z*p;
+        deriv->data.F32[8] = -2*t*4*z*pr3;
+    }
+    return(f);
+}
+
+bool PM_MODEL_LIMITS  (psVector **beta_lim, psVector **params_min, psVector **params_max)
+{
+
+    *beta_lim   = psVectorAlloc (9, PS_TYPE_F32);
+    *params_min = psVectorAlloc (9, PS_TYPE_F32);
+    *params_max = psVectorAlloc (9, PS_TYPE_F32);
+
+    beta_lim[0][0].data.F32[0] = 1000;
+    beta_lim[0][0].data.F32[1] = 10000;
+    beta_lim[0][0].data.F32[2] = 5;
+    beta_lim[0][0].data.F32[3] = 5;
+    beta_lim[0][0].data.F32[4] = 0.5;
+    beta_lim[0][0].data.F32[5] = 0.5;
+    beta_lim[0][0].data.F32[6] = 0.5;
+    beta_lim[0][0].data.F32[7] = 0.5;
+    beta_lim[0][0].data.F32[8] = 0.05;
+
+    params_min[0][0].data.F32[0] = -1000;
+    params_min[0][0].data.F32[1] = 0;
+    params_min[0][0].data.F32[2] = -100;
+    params_min[0][0].data.F32[3] = -100;
+    params_min[0][0].data.F32[4] = 0.01;
+    params_min[0][0].data.F32[5] = 0.01;
+    params_min[0][0].data.F32[6] = -5.0;
+    params_min[0][0].data.F32[7] = 0.5;
+    params_min[0][0].data.F32[8] = 0.001;
+
+    params_max[0][0].data.F32[0] = 1e5;
+    params_max[0][0].data.F32[1] = 1e6;
+    params_max[0][0].data.F32[2] = 1e4;  // this should be set by image dimensions!
+    params_max[0][0].data.F32[3] = 1e4;  // this should be set by image dimensions!
+    params_max[0][0].data.F32[4] = 2.0;
+    params_max[0][0].data.F32[5] = 2.0;
+    params_max[0][0].data.F32[6] = +3.0;
+    params_max[0][0].data.F32[7] = 5.0;
+    params_max[0][0].data.F32[8] = 0.5;
+
+    return (TRUE);
+}
+
+bool PM_MODEL_GUESS  (pmModel *model, pmSource *source)
+{
+
+    pmMoments *sMoments = source->moments;
+    pmPeak    *peak     = source->peak;
+    psF32     *params   = model->params->data.F32;
+
+    psEllipseAxes axes;
+    psEllipseShape shape;
+    psEllipseMoments moments;
+
+    moments.x2 = PS_SQR(sMoments->Sx);
+    moments.y2 = PS_SQR(sMoments->Sy);
+    moments.xy = sMoments->Sxy;
+
+    // solve the math to go from Moments To Shape
+    axes = psEllipseMomentsToAxes(moments);
+    shape = psEllipseAxesToShape(axes);
+
+    params[0] = sMoments->Sky;
+    params[1] = sMoments->Peak - sMoments->Sky;
+    params[2] = peak->x;
+    params[3] = peak->y;
+    params[4] = 1.0 / shape.sx;
+    params[5] = 1.0 / shape.sy;
+    params[6] = shape.sxy;
+
+    # if (0)
+
+        f1 = psImageEllipseContour (axes, peak->x, peak->y, source->pixels);
+    axes.major *= 2.0;
+    axes.minor *= 2.0;
+    f2 = psImageEllipseContour (axes, peak->x, peak->y, source->pixels);
+
+    if (f1 > f2) {
+        params[7] = PS_MIN (3.0, PS_MAX (0.5, log(2.0*(f1/f2) - 1.0) / log(2.0)));
+    } else {
+        params[7] = 0.5;
+    }
+    # endif
+
+    params[7] = 1.8;
+    params[8] = 0.1;
+
+
+    return(true);
+}
+
+psF64 PM_MODEL_FLUX (const psVector *params)
+{
+    float f, norm, z;
+
+    psF32 *PAR = params->data.F32;
+
+    psF64 A1   = PS_SQR(PAR[4]);
+    psF64 A2   = PS_SQR(PAR[5]);
+    psF64 A3   = PS_SQR(PAR[6]);
+    psF64 Area = 2.0 * M_PI / sqrt(A1*A2 - A3);
+    // Area is equivalent to 2 pi sigma^2
+
+    // the area needs to be multiplied by the integral of f(z)
+    norm = 0.0;
+    for (z = 0.005; z < 50; z += 0.01) {
+        psF32 pr = PAR[8]*z;
+        f = 1.0 / (1 + pow(z, PAR[7]) + PS_SQR(PS_SQR(pr)));
+        norm += f;
+    }
+    norm *= 0.01;
+
+    psF64 Flux = PAR[1] * Area * norm;
+
+    return(Flux);
+}
+
+// XXX need to define the radius along the major axis
+// define this function so it never returns Inf or NaN
+// return the radius which yields the requested flux
+psF64 PM_MODEL_RADIUS   (const psVector *params, psF64 flux)
+{
+    psF64 r, z = 0.0, pr, f;
+    psF32 *PAR = params->data.F32;
+
+    psEllipseAxes axes;
+    psEllipseShape shape;
+
+    if (flux <= 0)
+        return (1.0);
+    if (PAR[1] <= 0)
+        return (1.0);
+    if (flux >= PAR[1])
+        return (1.0);
+
+    // convert Sx,Sy,Sxy to major/minor axes
+    shape.sx = 1.0 / PAR[4];
+    shape.sy = 1.0 / PAR[5];
+    shape.sxy = PAR[6];
+
+    axes = psEllipseShapeToAxes (shape);
+    psF64 dr = 1.0 / axes.major;
+    psF64 limit = flux / PAR[1];
+
+    // XXX : we can do this faster with an intelligent starting choice
+    for (r = 0.0; r < 20.0; r += dr) {
+        z = PS_SQR(r);
+        pr = PAR[8]*z;
+        f = 1.0 / (1 + pow(z, PAR[7]) + PS_SQR(PS_SQR(pr)));
+        if (f < limit)
+            break;
+    }
+    psF64 radius = 2.0 * axes.major * sqrt (z);
+    if (isnan(radius)) {
+        fprintf (stderr, "error in code\n");
+    }
+    return (radius);
+}
+
+bool PM_MODEL_FROM_PSF  (pmModel *modelPSF, pmModel *modelFLT, pmPSF *psf)
+{
+
+    psF32 *out = modelPSF->params->data.F32;
+    psF32 *in  = modelFLT->params->data.F32;
+
+    out[0] = in[0];
+    out[1] = in[1];
+    out[2] = in[2];
+    out[3] = in[3];
+
+    for (int i = 4; i < 9; i++) {
+        psPolynomial2D *poly = psf->params->data[i-4];
+        // XXX: Verify this (from EAM change)
+        //out[i] = Polynomial2DEval_EAM(poly, out[2], out[3]);
+        out[i] = psPolynomial2DEval(poly, out[2], out[3]);
+    }
+    return(true);
+}
+
+bool PM_MODEL_FIT_STATUS  (pmModel *model)
+{
+
+    psF32 dP;
+    bool  status;
+    psEllipseAxes axes;
+    psEllipseShape shape;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    shape.sx = 1.0 / PAR[4];
+    shape.sy = 1.0 / PAR[5];
+    shape.sxy = PAR[6];
+
+    axes = psEllipseShapeToAxes (shape);
+
+    dP = 0;
+    dP += PS_SQR(dPAR[4] / PAR[4]);
+    dP += PS_SQR(dPAR[5] / PAR[5]);
+    dP += PS_SQR(dPAR[7] / PAR[7]);
+    dP = sqrt (dP);
+
+    status = true;
+    status &= (dP < 0.5);
+    status &= (PAR[1] > 0);
+    status &= ((dPAR[1]/PAR[1]) < 0.5);
+    status &= (fabs(PAR[8]) < 0.5);
+    status &= (dPAR[8] < 0.1);
+    status &= (axes.major > 1.41);
+    status &= (axes.minor > 1.41);
+    status &= ((axes.major / axes.minor) < 5.0);
+    status &= (PAR[7] > 0.5);
+
+    if (status)
+        return true;
+    return false;
+}
+
+// measure the flux for the elliptical contour
+psF64 psImageEllipseContour (psEllipseAxes axes, double xc, double yc, psImage *image)
+{
+
+    double t, dt, ct, st, xo, yo, value;
+    int N, Nt, x, y;
+
+    // choose dt to uniformly divide contour, with ~1 pix spacing at most
+    dt = asin (1 / axes.minor);
+    Nt = (int)(2*M_PI / dt) + 1;
+    dt = 2*M_PI / Nt;
+
+    ct = cos(axes.theta);
+    st = sin(axes.theta);
+    xo = xc - image->col0;
+    yo = yc - image->row0;
+
+    psVector *contour = psVectorAlloc (Nt, PS_TYPE_F32);
+    contour->n = contour->nalloc;
+    for (t = 0, N = 0; (t < 2*M_PI) && (N < Nt); t += dt) {
+        x = ct*axes.major*cos(t) + st*axes.minor*sin(t) + xo;
+        y = ct*axes.minor*sin(t) + st*axes.major*cos(t) + yo;
+        value = p_psImageGetElementF64(image, x, y);
+        if (isfinite(value)) {
+            contour->data.F32[N] = value;
+            N++;
+        }
+    }
+    contour->n = N;
+    // accept every pixel: double counting is not so problematic here...
+
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
+    psVectorStats (stats, contour, NULL, NULL, 0);
+    value = stats->sampleMedian;
+
+    psFree (stats);
+    psFree (contour);
+
+    return (value);
+}
+
+// XXX EAM : test version using flux contours to guess slope
+bool PM_MODEL_GUESS_HARD (pmModel *model, pmSource *source)
+{
+
+    pmMoments *sMoments = source->moments;
+    pmPeak    *peak     = source->peak;
+    psF32     *params   = model->params->data.F32;
+    float f1, f2;
+
+    psEllipseAxes axes;
+    psEllipseShape shape;
+    psEllipseMoments moments;
+
+    moments.x2 = PS_SQR(sMoments->Sx);
+    moments.y2 = PS_SQR(sMoments->Sy);
+    moments.xy = sMoments->Sxy;
+
+    // solve the math to go from Moments To Shape
+    axes = psEllipseMomentsToAxes(moments);
+    shape = psEllipseAxesToShape(axes);
+
+    params[0] = sMoments->Sky;
+    params[1] = sMoments->Peak - sMoments->Sky;
+    params[2] = peak->x;
+    params[3] = peak->y;
+    params[4] = 1.0 / shape.sx;
+    params[5] = 1.0 / shape.sy;
+    params[6] = shape.sxy;
+
+    f1 = psImageEllipseContour (axes, peak->x, peak->y, source->pixels);
+    axes.major *= 2.0;
+    axes.minor *= 2.0;
+    f2 = psImageEllipseContour (axes, peak->x, peak->y, source->pixels);
+
+    if (f1 > f2) {
+        params[7] = PS_MIN (3.0, PS_MAX (0.5, log(2.0*(f1/f2) - 1.0) / log(2.0)));
+    } else {
+        params[7] = 0.5;
+    }
+    params[8] = 0.1;
+
+    return(true);
+}
+
+# undef PM_MODEL_FUNC
+# undef PM_MODEL_FLUX
+# undef PM_MODEL_GUESS
+# undef PM_MODEL_LIMITS
+# undef PM_MODEL_RADIUS
+# undef PM_MODEL_FROM_PSF
+# undef PM_MODEL_FIT_STATUS
Index: /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_TGAUSS.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_TGAUSS.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_TGAUSS.c	(revision 15358)
@@ -0,0 +1,202 @@
+/******************************************************************************
+ * this file defines the TGAUSS source shape model (XXX need a better name!).  Note that these
+ * model functions are loaded by pmModelGroup.c using 'include', and thus need no 'include'
+ * statements of their own.  The models use a psVector to represent the set of parameters, with
+ * the sequence used to specify the meaning of the parameter.  The meaning of the parameters
+ * may thus vary depending on the specifics of the model.  All models which are used a PSF
+ * representations share a few parameters, for which # define names are listed in pmModel.h:
+ 
+   power-law with fixed slope and fitted amplitude
+   1 / (1 + z + kz^2.2)
+ 
+   * PM_PAR_SKY 0   - local sky : note that this is unused and may be dropped in the future
+   * PM_PAR_I0 1    - central intensity
+   * PM_PAR_XPOS 2  - X center of object
+   * PM_PAR_YPOS 3  - Y center of object
+   * PM_PAR_SXX 4   - X^2 term of elliptical contour (sqrt(2) / SigmaX)
+   * PM_PAR_SYY 5   - Y^2 term of elliptical contour (sqrt(2) / SigmaY)
+   * PM_PAR_SXY 6   - X*Y term of elliptical contour
+   * PM_PAR_7   7   - amplitude of high-order component (k)
+   *****************************************************************************/
+
+/***
+    XXXX the model in this file needs to be tested more carefully.
+    fix up the code to follow conventions in the other model function files.
+***/
+
+XXX broken code
+
+# define PM_MODEL_FUNC       pmModelFunc_TGAUSS
+# define PM_MODEL_FLUX       pmModelFlux_TGAUSS
+# define PM_MODEL_GUESS      pmModelGuess_TGAUSS
+# define PM_MODEL_LIMITS     pmModelLimits_TGAUSS
+# define PM_MODEL_RADIUS     pmModelRadius_TGAUSS
+# define PM_MODEL_FROM_PSF   pmModelFromPSF_TGAUSS
+# define PM_MODEL_FIT_STATUS pmModelFitStatus_TGAUSS
+
+psF64 PS_MODEL_FUNC (psVector *deriv,
+                     const psVector *params,
+                     const psVector *x)
+{
+    psF32 *PAR = params->data.F32;
+
+    psF32 X  = x->data.F32[0] - PAR[2];
+    psF32 Y  = x->data.F32[1] - PAR[3];
+    psF32 px = PAR[4]*X;
+    psF32 py = PAR[5]*Y;
+    psF32 z  = 0.5*PS_SQR(px) + 0.5*PS_SQR(py) + PAR[6]*X*Y;
+
+    psF32 p  = pow(z, 1.2);
+    psF32 r  = 1.0 / (1 + z + PAR[7]*z*p);
+    psF32 f  = PAR[1]*r + PAR[0];
+
+    if (deriv != NULL) {
+        // note difference from a pure gaussian: q = params->data.F32[1]*r
+        psF32 t = PAR[1]*r*r;
+        psF32 q = t*(1 + PAR[7]*2.2*p);
+
+        deriv->data.F32[0] = +1.0;
+        deriv->data.F32[1] = +r;
+        deriv->data.F32[2] = q*(2.0*px*PAR[4] + PAR[6]*Y);
+        deriv->data.F32[3] = q*(2.0*py*PAR[5] + PAR[6]*X);
+        deriv->data.F32[4] = -2.0*q*px*X;
+        deriv->data.F32[5] = -2.0*q*py*Y;
+        deriv->data.F32[6] = -q*X*Y;
+        deriv->data.F32[7] = -t*z*p;
+    }
+    return(f);
+}
+
+bool PM_MODEL_LIMITS  (psVector **beta_lim, psVector **params_min, psVector **params_max)
+{
+
+    *beta_lim   = psVectorAlloc (8, PS_TYPE_F32);
+    *params_min = psVectorAlloc (8, PS_TYPE_F32);
+    *params_max = psVectorAlloc (8, PS_TYPE_F32);
+
+    beta_lim[0][0].data.F32[PM_PAR_SKY] = 1000;
+    beta_lim[0][0].data.F32[PM_PAR_I0] = 3e6;
+    beta_lim[0][0].data.F32[PM_PAR_XPOS] = 5;
+    beta_lim[0][0].data.F32[PM_PAR_YPOS] = 5;
+    beta_lim[0][0].data.F32[PM_PAR_SXX] = 0.5;
+    beta_lim[0][0].data.F32[PM_PAR_SYY] = 0.5;
+    beta_lim[0][0].data.F32[PM_PAR_SXY] = 0.5;
+    beta_lim[0][0].data.F32[PM_PAR_7] = 0.5;
+
+    params_min[0][0].data.F32[PM_PAR_SKY] = -1000;
+    params_min[0][0].data.F32[PM_PAR_I0] = 0;
+    params_min[0][0].data.F32[PM_PAR_XPOS] = -100;
+    params_min[0][0].data.F32[PM_PAR_YPOS] = -100;
+    params_min[0][0].data.F32[PM_PAR_SXX] = 0.5;
+    params_min[0][0].data.F32[PM_PAR_SYY] = 0.5;
+    params_min[0][0].data.F32[PM_PAR_SXY] = -5.0;
+    params_min[0][0].data.F32[PM_PAR_7] = 0.1;
+
+    params_max[0][0].data.F32[PM_PAR_SKY] = 1e5;
+    params_max[0][0].data.F32[PM_PAR_I0] = 1e8;
+    params_max[0][0].data.F32[PM_PAR_XPOS] = 1e4;  // this should be set by image dimensions!
+    params_max[0][0].data.F32[PM_PAR_YPOS] = 1e4;  // this should be set by image dimensions!
+    params_max[0][0].data.F32[PM_PAR_SXX] = 100.0;
+    params_max[0][0].data.F32[PM_PAR_SYY] = 100.0;
+    params_max[0][0].data.F32[PM_PAR_SXY] = +5.0;
+    params_max[0][0].data.F32[PM_PAR_7] = 10.0;
+
+    return (TRUE);
+}
+
+bool PS_MODEL_GUESS  (psModel *model, psSource *source)
+{
+
+    psVector *params = model->params;
+
+    params->data.F32[0] = source->moments->Sky;
+    params->data.F32[1] = source->peak->counts - source->moments->Sky;
+    params->data.F32[2] = source->moments->x;
+    params->data.F32[3] = source->moments->y;
+    params->data.F32[4] = 1.0/source->moments->Sx;
+    params->data.F32[5] = 1.0/source->moments->Sy;
+    // params->data.F32[6] = source->moments->Sxy;
+    params->data.F32[6] = 0.0;
+    params->data.F32[7] = 5.0;
+    return(true);
+}
+
+psF64 PS_MODEL_FLUX (const psVector *params)
+{
+    psF64 A1   = 1 / PS_SQR(params->data.F32[4]);
+    psF64 A2   = 1 / PS_SQR(params->data.F32[5]);
+    psF64 A3   = params->data.F32[6];
+    psF64 Area = 2.0 * M_PI / sqrt(A1*A2 - PS_SQR(A3));
+    // Area is equivalent to 2 pi sigma^2
+
+    psF64 Flux = params->data.F32[1] * Area;
+
+    return(Flux);
+}
+
+// define this function so it never returns Inf or NaN
+// return the radius which yields the requested flux
+psF64 PS_MODEL_RADIUS   (const psVector *params, psF64 flux)
+{
+    if (flux <= 0)
+        return (1.0);
+    if (params->data.F32[1] <= 0)
+        return (1.0);
+    if (flux >= params->data.F32[1])
+        return (1.0);
+
+    psF64 sigma  = sqrt(2.0) * hypot (1.0 / params->data.F32[4], 1.0 / params->data.F32[5]);
+    psF64 radius = sigma * sqrt (2.0 * log(params->data.F32[1] / flux));
+    if (isnan(radius)) {
+        fprintf (stderr, "error in code\n");
+    }
+    return (radius);
+}
+
+bool PS_MODEL_FROM_PSF  (psModel *modelPSF, psModel *modelFLT, pmPSF *psf)
+{
+
+    psF32 *out = modelPSF->params->data.F32;
+    psF32 *in  = modelFLT->params->data.F32;
+
+    out[0] = in[0];
+    out[1] = in[1];
+    out[2] = in[2];
+    out[3] = in[3];
+
+    for (int i = 4; i < 8; i++) {
+        psPolynomial2D *poly = psf->params->data[i-4];
+        out[i] = Polynomial2DEval (poly, out[2], out[3]);
+    }
+    return(true);
+}
+
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+
+    psF32 dP;
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    dP = 0;
+    dP += PS_SQR(dPAR[PM_PAR_SXX] / PAR[PM_PAR_SXX]);
+    dP += PS_SQR(dPAR[PM_PAR_SYY] / PAR[PM_PAR_SYY]);
+    dP = sqrt (dP);
+
+    status = true;
+    status &= (dP < 0.5);
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+# undef PM_MODEL_FUNC
+# undef PM_MODEL_FLUX
+# undef PM_MODEL_GUESS
+# undef PM_MODEL_LIMITS
+# undef PM_MODEL_RADIUS
+# undef PM_MODEL_FROM_PSF
+# undef PM_MODEL_FIT_STATUS
Index: /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_WAUSS.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_WAUSS.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_WAUSS.c	(revision 15358)
@@ -0,0 +1,198 @@
+/******************************************************************************
+ * this file defines the WAUSS source shape model (XXX need a better name!).  Note that these
+ * model functions are loaded by pmModelGroup.c using 'include', and thus need no 'include'
+ * statements of their own.  The models use a psVector to represent the set of parameters, with
+ * the sequence used to specify the meaning of the parameter.  The meaning of the parameters
+ * may thus vary depending on the specifics of the model.  All models which are used a PSF
+ * representations share a few parameters, for which # define names are listed in pmModel.h:
+ 
+   power-law with fitted linear term
+   1 / (1 + Az + Bz^2 + z^3/6)
+ 
+   * PM_PAR_SKY 0   - local sky : note that this is unused and may be dropped in the future
+   * PM_PAR_I0 1    - central intensity
+   * PM_PAR_XPOS 2  - X center of object
+   * PM_PAR_YPOS 3  - Y center of object
+   * PM_PAR_SXX 4   - X^2 term of elliptical contour (SigmaX / sqrt(2))
+   * PM_PAR_SYY 5   - Y^2 term of elliptical contour (SigmaY / sqrt(2))
+   * PM_PAR_SXY 6   - X*Y term of elliptical contour
+   * PM_PAR_7   7   - amplitude of the linear component (A)
+   * PM_PAR_8   8   - amplitude of the quadratic component (B)
+   *****************************************************************************/
+
+/***
+    XXXX the model in this file needs to be tested more carefully.
+    fix up the code to follow conventions in the other model function files.
+***/
+
+XXX broken code
+
+# define PM_MODEL_FUNC       pmModelFunc_WAUSS
+# define PM_MODEL_FLUX       pmModelFlux_WAUSS
+# define PM_MODEL_GUESS      pmModelGuess_WAUSS
+# define PM_MODEL_LIMITS     pmModelLimits_WAUSS
+# define PM_MODEL_RADIUS     pmModelRadius_WAUSS
+# define PM_MODEL_FROM_PSF   pmModelFromPSF_WAUSS
+# define PM_MODEL_FIT_STATUS pmModelFitStatus_WAUSS
+
+psF64 PS_MODEL_FUNC (psVector *deriv,
+                     const psVector *params,
+                     const psVector *x)
+{
+    psF32 X = x->data.F32[0] - params->data.F32[2];
+    psF32 Y = x->data.F32[1] - params->data.F32[2];
+    psF32 px = params->data.F32[4]*X;
+    psF32 py = params->data.F32[5]*Y;
+    psF32 z = 0.5*PS_SQR(px) + 0.5*PS_SQR(py) + params->data.F32[6]*X*Y;
+    psF32 t = 0.5*z*z*(1.0 + params->data.F32[8]*z/3.0);
+    psF32 r = 1.0 / (1.0 + z + params->data.F32[7]*t); /* exp (-Z) */
+    psF32 f = params->data.F32[1]*r + params->data.F32[0];
+
+    if (deriv != NULL) {
+        // note difference from gaussian: q = params->data.F32[1]*r
+        psF32 q = params->data.F32[1]*r*r*(1.0 + params->data.F32[7]*z*(1.0 + params->data.F32[8]*z/2.0));
+        deriv->data.F32[0] = +1.0;
+        deriv->data.F32[1] = +r;
+        deriv->data.F32[2] = q*(2.0*px*params->data.F32[4] + params->data.F32[6]*Y);
+        deriv->data.F32[3] = q*(2.0*py*params->data.F32[5] + params->data.F32[6]*X);
+        deriv->data.F32[4] = -2.0*q*px*X;
+        deriv->data.F32[5] = -2.0*q*py*Y;
+        deriv->data.F32[6] = -q*X*Y;
+        deriv->data.F32[7] = -100.0*params->data.F32[1]*r*r*t;
+        deriv->data.F32[8] = -100.0*params->data.F32[1]*r*r*params->data.F32[7]*(z*z*z)/6.0;
+        // The values of 100 dampen the swing of params->data.F32[7,8] */
+    }
+    return(f);
+}
+
+bool PM_MODEL_LIMITS  (psVector **beta_lim, psVector **params_min, psVector **params_max)
+{
+
+    *beta_lim   = psVectorAlloc (8, PS_TYPE_F32);
+    *params_min = psVectorAlloc (8, PS_TYPE_F32);
+    *params_max = psVectorAlloc (8, PS_TYPE_F32);
+
+    beta_lim[0][0].data.F32[PM_PAR_SKY] = 1000;
+    beta_lim[0][0].data.F32[PM_PAR_I0] = 3e6;
+    beta_lim[0][0].data.F32[PM_PAR_XPOS] = 5;
+    beta_lim[0][0].data.F32[PM_PAR_YPOS] = 5;
+    beta_lim[0][0].data.F32[PM_PAR_SXX] = 0.5;
+    beta_lim[0][0].data.F32[PM_PAR_SYY] = 0.5;
+    beta_lim[0][0].data.F32[PM_PAR_SXY] = 0.5;
+    beta_lim[0][0].data.F32[PM_PAR_7] = 0.5;
+
+    params_min[0][0].data.F32[PM_PAR_SKY] = -1000;
+    params_min[0][0].data.F32[PM_PAR_I0] = 0;
+    params_min[0][0].data.F32[PM_PAR_XPOS] = -100;
+    params_min[0][0].data.F32[PM_PAR_YPOS] = -100;
+    params_min[0][0].data.F32[PM_PAR_SXX] = 0.5;
+    params_min[0][0].data.F32[PM_PAR_SYY] = 0.5;
+    params_min[0][0].data.F32[PM_PAR_SXY] = -5.0;
+    params_min[0][0].data.F32[PM_PAR_7] = 0.1;
+
+    params_max[0][0].data.F32[PM_PAR_SKY] = 1e5;
+    params_max[0][0].data.F32[PM_PAR_I0] = 1e8;
+    params_max[0][0].data.F32[PM_PAR_XPOS] = 1e4;  // this should be set by image dimensions!
+    params_max[0][0].data.F32[PM_PAR_YPOS] = 1e4;  // this should be set by image dimensions!
+    params_max[0][0].data.F32[PM_PAR_SXX] = 100.0;
+    params_max[0][0].data.F32[PM_PAR_SYY] = 100.0;
+    params_max[0][0].data.F32[PM_PAR_SXY] = +5.0;
+    params_max[0][0].data.F32[PM_PAR_7] = 10.0;
+
+    return (TRUE);
+}
+
+bool PS_MODEL_GUESS  (psModel *model, psSource *source)
+{
+
+    psVector *params = model->params;
+
+    params->data.F32[0] = source->moments->Sky;
+    params->data.F32[1] = source->peak->counts - source->moments->Sky;
+    params->data.F32[2] = source->moments->x;
+    params->data.F32[3] = source->moments->y;
+    params->data.F32[4] = sqrt(2.0) / source->moments->Sx;
+    params->data.F32[5] = sqrt(2.0) / source->moments->Sy;
+    params->data.F32[6] = source->moments->Sxy;
+    // XXX: What are these?
+    // params->data.F32[7] = B2;
+    // params->data.F32[8] = B3;
+    return(true);
+}
+
+// this is probably wrong since it uses the gauss integral 2 pi sigma^2
+psF64 PS_MODEL_FLUX (const psVector *params)
+{
+    psF64 A1   = 1 / PS_SQR(params->data.F32[4]);
+    psF64 A2   = 1 / PS_SQR(params->data.F32[5]);
+    psF64 A3   = params->data.F32[6];
+    psF64 Area = 2.0 * M_PI / sqrt(A1*A2 - PS_SQR(A3));
+    // Area is equivalent to 2 pi sigma^2
+
+    psF64 Flux = params->data.F32[1] * Area;
+
+    return(Flux);
+}
+
+// return the radius which yields the requested flux
+psF64 PS_MODEL_RADIUS   (const psVector *params, psF64 flux)
+{
+    if (flux <= 0)
+        return (1.0);
+    if (params->data.F32[1] <= 0)
+        return (1.0);
+    if (flux >= params->data.F32[1] <= 0)
+        return (1.0);
+
+    psF64 sigma  = sqrt(2.0) * hypot (1.0 / params->data.F32[4], 1.0 / params->data.F32[5]);
+    psF64 radius = sigma * sqrt (2.0 * log(params->data.F32[1] / flux));
+    return (radius);
+}
+
+bool PS_MODEL_FROM_PSF  (psModel *modelPSF, psModel *modelFLT, pmPSF *psf)
+{
+
+    psF32 *out = modelPSF->params->data.F32;
+    psF32 *in  = modelFLT->params->data.F32;
+
+    out[0] = in[0];
+    out[1] = in[1];
+    out[2] = in[2];
+    out[3] = in[3];
+
+    for (int i = 4; i < 9; i++) {
+        psPolynomial2D *poly = psf->params->data[i-4];
+        out[i] = Polynomial2DEval (poly, out[2], out[3]);
+    }
+    return(true);
+}
+
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+
+    psF32 dP;
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    dP = 0;
+    dP += PS_SQR(dPAR[PM_PAR_SXX] / PAR[PM_PAR_SXX]);
+    dP += PS_SQR(dPAR[PM_PAR_SYY] / PAR[PM_PAR_SYY]);
+    dP = sqrt (dP);
+
+    status = true;
+    status &= (dP < 0.5);
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+# undef PM_MODEL_FUNC
+# undef PM_MODEL_FLUX
+# undef PM_MODEL_GUESS
+# undef PM_MODEL_LIMITS
+# undef PM_MODEL_RADIUS
+# undef PM_MODEL_FROM_PSF
+# undef PM_MODEL_FIT_STATUS
Index: /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_ZGAUSS.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_ZGAUSS.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/models/pmModel_ZGAUSS.c	(revision 15358)
@@ -0,0 +1,251 @@
+/******************************************************************************
+ * this file defines the ZGAUSS source shape model (XXX need a better name!).  Note that these
+ * model functions are loaded by pmModelGroup.c using 'include', and thus need no 'include'
+ * statements of their own.  The models use a psVector to represent the set of parameters, with
+ * the sequence used to specify the meaning of the parameter.  The meaning of the parameters
+ * may thus vary depending on the specifics of the model.  All models which are used a PSF
+ * representations share a few parameters, for which # define names are listed in pmModel.h:
+ 
+   power-law with fitted slope and tidal cutoff
+   1 / (1 + z^N + (Az)^4)
+ 
+   * PM_PAR_SKY 0   - local sky : note that this is unused and may be dropped in the future
+   * PM_PAR_I0 1    - central intensity
+   * PM_PAR_XPOS 2  - X center of object
+   * PM_PAR_YPOS 3  - Y center of object
+   * PM_PAR_SXX 4   - X^2 term of elliptical contour (sqrt(2) / SigmaX)
+   * PM_PAR_SYY 5   - Y^2 term of elliptical contour (sqrt(2) / SigmaY)
+   * PM_PAR_SXY 6   - X*Y term of elliptical contour
+   * PM_PAR_7   7   - slope of power-law component (N)
+   *****************************************************************************/
+
+/***
+    XXXX the model in this file needs to be tested more carefully.
+    fix up the code to follow conventions in the other model function files.
+***/
+
+XXX broken code
+
+# define PM_MODEL_FUNC       pmModelFunc_ZGAUSS
+# define PM_MODEL_FLUX       pmModelFlux_ZGAUSS
+# define PM_MODEL_GUESS      pmModelGuess_ZGAUSS
+# define PM_MODEL_LIMITS     pmModelLimits_ZGAUSS
+# define PM_MODEL_RADIUS     pmModelRadius_ZGAUSS
+# define PM_MODEL_FROM_PSF   pmModelFromPSF_ZGAUSS
+# define PM_MODEL_FIT_STATUS pmModelFitStatus_ZGAUSS
+
+# define PAR8 0.1
+
+psF64 PS_MODEL_FUNC (psVector *deriv,
+                     const psVector *params,
+                     const psVector *x)
+{
+    psF32 *PAR = params->data.F32;
+
+    psF32 X  = x->data.F32[0] - PAR[2];
+    psF32 Y  = x->data.F32[1] - PAR[3];
+    psF32 px = PAR[4]*X;
+    psF32 py = PAR[5]*Y;
+    psF32 z  = 0.5*PS_SQR(px) + 0.5*PS_SQR(py) + PAR[6]*X*Y;
+
+    psF32 pr = PAR8*z;
+    psF32 p  = pow(z, PAR[7] - 1.0);
+    psF32 r  = 1.0 / (1 + z*p + SQ(SQ(pr)));
+    psF32 f  = PAR[1]*r + PAR[0];
+
+    if (deriv != NULL) {
+        // note difference from a pure gaussian: q = params->data.F32[1]*r
+        psF32 t = PAR[1]*r*r;
+        psF32 q = t*(PAR[7]*p + 4*PAR8*pr*pr*pr);
+
+        deriv->data.F32[0] = +1.0;
+        deriv->data.F32[1] = +r;
+        deriv->data.F32[2] = q*(2.0*px*PAR[4] + PAR[6]*Y);
+        deriv->data.F32[3] = q*(2.0*py*PAR[5] + PAR[6]*X);
+        deriv->data.F32[4] = -q*px*X;
+        deriv->data.F32[5] = -q*py*Y;
+        deriv->data.F32[6] = -q*X*Y;
+        deriv->data.F32[7] = -t*log(z)*z*p;
+    }
+    return(f);
+}
+
+bool PM_MODEL_LIMITS  (psVector **beta_lim, psVector **params_min, psVector **params_max)
+{
+
+    *beta_lim   = psVectorAlloc (8, PS_TYPE_F32);
+    *params_min = psVectorAlloc (8, PS_TYPE_F32);
+    *params_max = psVectorAlloc (8, PS_TYPE_F32);
+
+    beta_lim[0][0].data.F32[PM_PAR_SKY] = 1000;
+    beta_lim[0][0].data.F32[PM_PAR_I0] = 3e6;
+    beta_lim[0][0].data.F32[PM_PAR_XPOS] = 5;
+    beta_lim[0][0].data.F32[PM_PAR_YPOS] = 5;
+    beta_lim[0][0].data.F32[PM_PAR_SXX] = 0.5;
+    beta_lim[0][0].data.F32[PM_PAR_SYY] = 0.5;
+    beta_lim[0][0].data.F32[PM_PAR_SXY] = 0.5;
+    beta_lim[0][0].data.F32[PM_PAR_7] = 0.5;
+
+    params_min[0][0].data.F32[PM_PAR_SKY] = -1000;
+    params_min[0][0].data.F32[PM_PAR_I0] = 0;
+    params_min[0][0].data.F32[PM_PAR_XPOS] = -100;
+    params_min[0][0].data.F32[PM_PAR_YPOS] = -100;
+    params_min[0][0].data.F32[PM_PAR_SXX] = 0.5;
+    params_min[0][0].data.F32[PM_PAR_SYY] = 0.5;
+    params_min[0][0].data.F32[PM_PAR_SXY] = -5.0;
+    params_min[0][0].data.F32[PM_PAR_7] = 0.1;
+
+    params_max[0][0].data.F32[PM_PAR_SKY] = 1e5;
+    params_max[0][0].data.F32[PM_PAR_I0] = 1e8;
+    params_max[0][0].data.F32[PM_PAR_XPOS] = 1e4;  // this should be set by image dimensions!
+    params_max[0][0].data.F32[PM_PAR_YPOS] = 1e4;  // this should be set by image dimensions!
+    params_max[0][0].data.F32[PM_PAR_SXX] = 100.0;
+    params_max[0][0].data.F32[PM_PAR_SYY] = 100.0;
+    params_max[0][0].data.F32[PM_PAR_SXY] = +5.0;
+    params_max[0][0].data.F32[PM_PAR_7] = 10.0;
+
+    return (TRUE);
+}
+
+bool PS_MODEL_GUESS  (psModel *model, psSource *source)
+{
+
+    psVector *params = model->params;
+
+    psEllipseAxes axes;
+    psEllipseShape shape;
+    psEllipseMoments moments;
+
+    moments.x2 = PS_SQR(source->moments->Sx);
+    moments.y2 = PS_SQR(source->moments->Sy);
+    moments.xy = source->moments->Sxy;
+
+    axes = psEllipseMomentsToAxes(moments);
+    shape = psEllipseAxesToShape(axes);
+
+    params->data.F32[0] = source->moments->Sky;
+    params->data.F32[1] = source->peak->counts - source->moments->Sky;
+    params->data.F32[2] = source->moments->x;
+    params->data.F32[3] = source->moments->y;
+    params->data.F32[4] = 1.0 / shape.sx;
+    params->data.F32[5] = 1.0 / shape.sy;
+    params->data.F32[6] = shape.sxy;
+    params->data.F32[7] = 1.9;
+    return(true);
+}
+
+psF64 PS_MODEL_FLUX (const psVector *params)
+{
+    float f, norm, z;
+
+    psF32 *PAR = params->data.F32;
+
+    psF64 A1   = PS_SQR(PAR[4]);
+    psF64 A2   = PS_SQR(PAR[5]);
+    psF64 A3   = PS_SQR(PAR[6]);
+    psF64 Area = 2.0 * M_PI / sqrt(A1*A2 - A3);
+    // Area is equivalent to 2 pi sigma^2
+
+    // the area needs to be multiplied by the integral of f(z)
+    norm = 0.0;
+    psF32 pr = PAR8*z;
+    for (z = 0.005; z < 50; z += 0.01) {
+        f = 1.0 / (1 + pow(z, PAR[7]) + SQ(SQ(pr)));
+        norm += f;
+    }
+    norm *= 0.01;
+
+    psF64 Flux = PAR[1] * Area * norm;
+
+    return(Flux);
+}
+
+// XXX need to define the radius along the major axis
+// define this function so it never returns Inf or NaN
+// return the radius which yields the requested flux
+psF64 PS_MODEL_RADIUS   (const psVector *params, psF64 flux)
+{
+    psF64 r, z, pr, f;
+    psF32 *PAR = params->data.F32;
+
+    psEllipseAxes axes;
+    psEllipseShape shape;
+
+    if (flux <= 0)
+        return (1.0);
+    if (PAR[1] <= 0)
+        return (1.0);
+    if (flux >= PAR[1])
+        return (1.0);
+
+    // convert Sx,Sy,Sxy to major/minor axes
+    shape.sx = 1.0 / PAR[4];
+    shape.sy = 1.0 / PAR[5];
+    shape.sxy = PAR[6];
+
+    axes = psEllipseShapeToAxes (shape);
+    psF64 dr = 1.0 / axes.major;
+    psF64 limit = flux / PAR[1];
+
+    // XXX : we can do this faster with an intelligent starting choice
+    for (r = 0.0; r < 20.0; r += dr) {
+        z = SQ(r);
+        pr = PAR8*z;
+        f = 1.0 / (1 + pow(z, PAR[7]) + SQ(SQ(pr)));
+        if (f < limit)
+            break;
+    }
+    psF64 radius = 2.0 * axes.major * sqrt (z);
+    if (isnan(radius)) {
+        fprintf (stderr, "error in code\n");
+    }
+    return (radius);
+}
+
+bool PS_MODEL_FROM_PSF  (psModel *modelPSF, psModel *modelFLT, pmPSF *psf)
+{
+
+    psF32 *out = modelPSF->params->data.F32;
+    psF32 *in  = modelFLT->params->data.F32;
+
+    out[0] = in[0];
+    out[1] = in[1];
+    out[2] = in[2];
+    out[3] = in[3];
+
+    for (int i = 4; i < 8; i++) {
+        psPolynomial2D *poly = psf->params->data[i-4];
+        out[i] = Polynomial2DEval (poly, out[2], out[3]);
+    }
+    return(true);
+}
+
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+
+    psF32 dP;
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    dP = 0;
+    dP += PS_SQR(dPAR[PM_PAR_SXX] / PAR[PM_PAR_SXX]);
+    dP += PS_SQR(dPAR[PM_PAR_SYY] / PAR[PM_PAR_SYY]);
+    dP = sqrt (dP);
+
+    status = true;
+    status &= (dP < 0.5);
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    return status;
+}
+
+# undef PM_MODEL_FUNC
+# undef PM_MODEL_FLUX
+# undef PM_MODEL_GUESS
+# undef PM_MODEL_LIMITS
+# undef PM_MODEL_RADIUS
+# undef PM_MODEL_FROM_PSF
+# undef PM_MODEL_FIT_STATUS
Index: /branches/eam_branch_20071023/psModules/src/objects/pmGrowthCurve.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmGrowthCurve.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmGrowthCurve.c	(revision 15358)
@@ -0,0 +1,72 @@
+/** @file  pmGrowthCurve.c
+ *
+ *  Measure the curve-of-growth for sources
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-10-03 00:42:40 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "psVectorBracket.h"
+
+static void pmGrowthCurveFree (pmGrowthCurve *growth)
+{
+    if (growth == NULL)
+        return;
+
+    psFree (growth->radius);
+    psFree (growth->apMag);
+    return;
+}
+
+bool psMemCheckGrowthCurve(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc)pmGrowthCurveFree );
+}
+
+pmGrowthCurve *pmGrowthCurveAlloc (psF32 minRadius, psF32 maxRadius, psF32 refRadius)
+{
+
+    pmGrowthCurve *growth = psAlloc (sizeof(pmGrowthCurve));
+    psMemSetDeallocator(growth, (psFreeFunc) pmGrowthCurveFree);
+
+    // fractional pixel radii are not well defined; use integer steps
+    growth->radius = psVectorCreate (NULL, minRadius, maxRadius, 1.0, PS_TYPE_F32);
+    growth->apMag  = psVectorAlloc (growth->radius->n, PS_TYPE_F32);
+
+    // XXX may want to extend this to allow for a different refRadius;
+    growth->refRadius = refRadius;
+    growth->maxRadius = maxRadius;
+    growth->apLoss = 0.0;
+    growth->fitMag = 0.0;
+    return growth;
+}
+
+psF32 pmGrowthCurveCorrect(pmGrowthCurve *growth, psF32 radius)
+{
+    PS_ASSERT_PTR_NON_NULL(growth, NAN);
+    float apRad = psVectorInterpolate (growth->radius, growth->apMag, radius);
+    float apCor = growth->apRef - apRad;
+    return apCor;
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmGrowthCurve.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmGrowthCurve.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmGrowthCurve.h	(revision 15358)
@@ -0,0 +1,33 @@
+/* @file  pmGrowthCurve.h
+ * @brief functions to manipulate the curve-of-growth data
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:15 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+# ifndef PM_GROWTH_CURVE_H
+# define PM_GROWTH_CURVE_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef struct
+{
+    psVector *radius;
+    psVector *apMag;
+    psF32 refRadius;
+    psF32 maxRadius;
+    psF32 fitMag;
+    psF32 apRef;   // apMag[refRadius]
+    psF32 apLoss;  // fitMag - apRef
+}
+pmGrowthCurve;
+
+pmGrowthCurve *pmGrowthCurveAlloc (psF32 minRadius, psF32 maxRadius, psF32 refRadius);
+psF32 pmGrowthCurveCorrect (pmGrowthCurve *growth, psF32 radius);
+
+/// @}
+# endif /* PM_GROWTH_CURVE_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmModel.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmModel.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmModel.c	(revision 15358)
@@ -0,0 +1,360 @@
+/** @file  pmModel.c
+ *
+ *  Functions to define and manipulate object models
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:04:07 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmModelClass.h"
+
+static void modelFree(pmModel *tmp)
+{
+    psTrace("psModules.objects", 4, "---- %s() begin ----\n", __func__);
+    psFree(tmp->params);
+    psFree(tmp->dparams);
+    psTrace("psModules.objects", 4, "---- %s() end ----\n", __func__);
+}
+
+/******************************************************************************
+pmModelAlloc(): Allocate the pmModel structure, along with its parameters,
+and initialize the type member.  Initialize the params to 0.0.
+*****************************************************************************/
+pmModel *pmModelAlloc(pmModelType type)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+
+    pmModel *tmp = (pmModel *) psAlloc(sizeof(pmModel));
+    psMemSetDeallocator(tmp, (psFreeFunc) modelFree);
+
+    pmModelClass *class = pmModelClassSelect (type);
+    if (class == NULL) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return(NULL);
+    }
+
+    tmp->type = type;
+    tmp->chisq = 0.0;
+    tmp->nDOF  = 0;
+    tmp->nIter = 0;
+    tmp->radiusFit = 0;
+    tmp->flags = PM_MODEL_STATUS_NONE;
+    tmp->residuals = NULL;              // XXX should the model own this memory?
+
+    psS32 Nparams = pmModelClassParameterCount(type);
+    assert (Nparams);
+
+    tmp->params  = psVectorAlloc(Nparams, PS_TYPE_F32);
+    tmp->dparams = psVectorAlloc(Nparams, PS_TYPE_F32);
+    assert (tmp->params);
+    assert (tmp->dparams);
+
+    for (psS32 i = 0; i < tmp->params->n; i++) {
+        tmp->params->data.F32[i] = 0.0;
+        tmp->dparams->data.F32[i] = 0.0;
+    }
+
+    tmp->modelFunc   	    = class->modelFunc;
+    tmp->modelFlux   	    = class->modelFlux;
+    tmp->modelRadius 	    = class->modelRadius;
+    tmp->modelLimits 	    = class->modelLimits;
+    tmp->modelGuess         = class->modelGuess;
+    tmp->modelFromPSF       = class->modelFromPSF;
+    tmp->modelParamsFromPSF = class->modelParamsFromPSF;
+    tmp->modelFitStatus     = class->modelFitStatus;
+
+    psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+    return(tmp);
+}
+
+// copy model to a new structure
+pmModel *pmModelCopy (pmModel *model)
+{
+
+    pmModel *new = pmModelAlloc (model->type);
+
+    new->chisq     = model->chisq;
+    new->nDOF      = model->nDOF;
+    new->nIter     = model->nIter;
+    new->flags     = model->flags;
+    new->radiusFit = model->radiusFit;
+
+    for (int i = 0; i < new->params->n; i++) {
+        new->params->data.F32[i]  = model->params->data.F32[i];
+        new->dparams->data.F32[i] = model->dparams->data.F32[i];
+    }
+
+    // note that model->residuals is just a reference
+    new->residuals = model->residuals;
+
+    return (new);
+}
+
+/******************************************************************************
+    pmModelEval(source, level, row): evaluates the model function at the specified coords.
+
+    NOTE: The coords are in subImage source->pixel coords, not image coords.
+
+    XXX: Use static vectors for x (NO: needs to be thread safe)
+*****************************************************************************/
+psF32 pmModelEval(pmModel *model, psImage *image, psS32 col, psS32 row)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(image, false);
+    PS_ASSERT_PTR_NON_NULL(model, false);
+    PS_ASSERT_PTR_NON_NULL(model->params, false);
+
+    // Allocate the x coordinate structure and convert row/col to image space.
+    //
+    psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+    x->data.F32[0] = (psF32) (col + image->col0);
+    x->data.F32[1] = (psF32) (row + image->row0);
+    psF32 tmpF;
+
+    tmpF = model->modelFunc (NULL, model->params, x);
+    psFree(x);
+    psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+    return(tmpF);
+}
+
+psF32 pmModelEvalWithOffset(pmModel *model, psImage *image, psS32 col, psS32 row, int dx, int dy)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(image, false);
+    PS_ASSERT_PTR_NON_NULL(model, false);
+    PS_ASSERT_PTR_NON_NULL(model->params, false);
+
+    // Allocate the x coordinate structure and convert row/col to image space.
+    //
+    psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+    x->data.F32[0] = (psF32) (col + image->col0 + dx);
+    x->data.F32[1] = (psF32) (row + image->row0 + dy);
+    psF32 tmpF;
+
+    tmpF = model->modelFunc (NULL, model->params, x);
+    psFree(x);
+    psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+    return(tmpF);
+}
+
+static bool AddOrSubModel(psImage *image,
+                          psImage *mask,
+                          pmModel *model,
+                          pmModelOpMode mode,
+                          bool add,
+                          psMaskType maskVal,
+			  int dx,
+			  int dy
+    )
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+
+    PS_ASSERT_PTR_NON_NULL(model, false);
+    PS_ASSERT_IMAGE_NON_NULL(image, false);
+    PS_ASSERT_IMAGE_TYPE(image, PS_TYPE_F32, false);
+
+    psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+    psVector *params = model->params;
+
+    psS32 imageCol;
+    psS32 imageRow;
+    psF32 pixelValue;
+
+    // save original values; restore before returning
+    // use the true source position for the residual model
+    // the PSF model has presumably already been set for this coordinate
+    float xPos    = params->data.F32[PM_PAR_XPOS];
+    float yPos    = params->data.F32[PM_PAR_YPOS];
+    float IoSave  = params->data.F32[PM_PAR_I0];
+    float skySave = params->data.F32[PM_PAR_SKY];
+
+    if (mode & PM_MODEL_OP_NORM) {
+        params->data.F32[PM_PAR_I0] = 1.0;
+    }
+    if (!(mode & PM_MODEL_OP_SKY)) {
+        params->data.F32[PM_PAR_SKY] = 0.0;
+    }
+    if (mode & PM_MODEL_OP_CENTER) {
+        params->data.F32[PM_PAR_XPOS] = image->col0 + 0.5*image->numCols;
+        params->data.F32[PM_PAR_YPOS] = image->row0 + 0.5*image->numRows;
+    }
+
+    // use these values for this realization
+    float xCenter  = params->data.F32[PM_PAR_XPOS];
+    float yCenter  = params->data.F32[PM_PAR_YPOS];
+    float Io       = params->data.F32[PM_PAR_I0];
+
+    int xBin = 1;
+    int yBin = 1;
+    float xResidCenter = 0.0;
+    float yResidCenter = 0.0;
+
+    psImageInterpolateOptions *Ro = NULL;
+    psImageInterpolateOptions *Rx = NULL;
+    psImageInterpolateOptions *Ry = NULL;
+    if (model->residuals && (mode & (PM_MODEL_OP_RES0 | PM_MODEL_OP_RES1))) {
+        Ro = psImageInterpolateOptionsAlloc(
+            PS_INTERPOLATE_BILINEAR,
+            model->residuals->Ro, NULL, NULL, 0, 0.0, 0.0, 1, 0, 0.0);
+        Rx = psImageInterpolateOptionsAlloc(
+            PS_INTERPOLATE_BILINEAR,
+            model->residuals->Rx, NULL, NULL, 0, 0.0, 0.0, 1, 0, 0.0);
+        Ry = psImageInterpolateOptionsAlloc(
+            PS_INTERPOLATE_BILINEAR,
+            model->residuals->Ry, NULL, NULL, 0, 0.0, 0.0, 1, 0, 0.0);
+
+        xBin = model->residuals->xBin;
+        yBin = model->residuals->yBin;
+        xResidCenter = model->residuals->xCenter;
+        yResidCenter = model->residuals->yCenter;
+    }
+
+    for (psS32 iy = 0; iy < image->numRows; iy++) {
+        for (psS32 ix = 0; ix < image->numCols; ix++) {
+            if ((mask != NULL) && (mask->data.U8[iy][ix] & maskVal))
+                continue;
+
+            // Convert i/j to image coord space:
+            // XXX should we use using 0.5 pixel offset?
+            imageCol = ix + image->col0 + dx;
+            imageRow = iy + image->row0 + dy;
+
+            x->data.F32[0] = (float) imageCol;
+            x->data.F32[1] = (float) imageRow;
+
+            pixelValue = 0.0;
+
+            // add in the desired components for this coordinate
+            if (mode & PM_MODEL_OP_FUNC) {
+                pixelValue += model->modelFunc (NULL, params, x);
+            }
+
+            // get the contribution from the residual model
+            if (Ro) {
+                // fractional image position
+                float ox = xBin*(imageCol + 0.5 - xCenter) + xResidCenter;
+                float oy = yBin*(imageRow + 0.5 - yCenter) + yResidCenter;
+
+                if (mode & PM_MODEL_OP_RES0) {
+                    psU8 mflux = 0;
+                    double Fo = 0.0;
+                    psImageInterpolate (&Fo, NULL, &mflux, ox, oy, Ro);
+                    if (!mflux && isfinite(Fo)) {
+                        pixelValue += Io*Fo;
+                    }
+                }
+                if (mode & PM_MODEL_OP_RES1) {
+                    psU8 mflux = 0;
+                    double Fx = 0.0;
+                    double Fy = 0.0;
+                    psImageInterpolate (&Fx, NULL, &mflux, ox, oy, Rx);
+                    psImageInterpolate (&Fy, NULL, &mflux, ox, oy, Ry);
+                    if (!mflux && isfinite(Fx) && isfinite(Fy)) {
+                        pixelValue += Io*(xPos*Fx + yPos*Fy);
+                    }
+                }
+            }
+
+            // add or subtract the value
+            if (add) {
+                image->data.F32[iy][ix] += pixelValue;
+            } else {
+                image->data.F32[iy][ix] -= pixelValue;
+            }
+        }
+    }
+
+    // restore original values
+    params->data.F32[PM_PAR_I0]   = IoSave;
+    params->data.F32[PM_PAR_SKY]  = skySave;
+    params->data.F32[PM_PAR_XPOS] = xPos;
+    params->data.F32[PM_PAR_YPOS] = yPos;
+
+    psFree(x);
+    psFree(Ro);
+    psFree(Rx);
+    psFree(Ry);
+    psTrace("psModules.objects", 3, "---- %s(true) end ----\n", __func__);
+    return(true);
+}
+
+/******************************************************************************
+ *****************************************************************************/
+bool pmModelAdd(psImage *image,
+                psImage *mask,
+                pmModel *model,
+                pmModelOpMode mode,
+                psMaskType maskVal)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    psBool rc = AddOrSubModel(image, mask, model, mode, true, maskVal, 0.0, 0.0);
+    psTrace("psModules.objects", 3, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
+/******************************************************************************
+ *****************************************************************************/
+bool pmModelSub(psImage *image,
+                psImage *mask,
+                pmModel *model,
+                pmModelOpMode mode,
+                psMaskType maskVal)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    psBool rc = AddOrSubModel(image, mask, model, mode, false, maskVal, 0.0, 0.0);
+    psTrace("psModules.objects", 3, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
+/******************************************************************************
+ *****************************************************************************/
+bool pmModelAddWithOffset(psImage *image,
+			  psImage *mask,
+			  pmModel *model,
+			  pmModelOpMode mode,
+			  psMaskType maskVal,
+			  int dx,
+			  int dy)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    psBool rc = AddOrSubModel(image, mask, model, mode, true, maskVal, dx, dy);
+    psTrace("psModules.objects", 3, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
+/******************************************************************************
+ *****************************************************************************/
+bool pmModelSubWithOffset(psImage *image,
+			  psImage *mask,
+			  pmModel *model,
+			  pmModelOpMode mode,
+			  psMaskType maskVal,
+			  int dx,
+			  int dy)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    psBool rc = AddOrSubModel(image, mask, model, mode, false, maskVal, dx, dy);
+    psTrace("psModules.objects", 3, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmModel.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmModel.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmModel.h	(revision 15358)
@@ -0,0 +1,197 @@
+/* @file  pmModel.h
+ * @brief Functions to define and manipulate object models
+ *
+ * @author GLG, MHPCC
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-08-24 00:11:02 $
+ *
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_MODEL_H
+# define PM_MODEL_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/* pointers for the functions types below are supplied to each pmModel, and can be used by
+   the programmer without needing to know the model class */
+
+typedef enum {
+    PM_MODEL_STATUS_NONE         = 0x00, ///< model fit not yet attempted, no other info
+    PM_MODEL_STATUS_FITTED       = 0x01, ///< model fit completed
+    PM_MODEL_STATUS_NONCONVERGE  = 0x02, ///< model fit did not converge
+    PM_MODEL_STATUS_OFFIMAGE     = 0x04, ///< model fit drove out of range
+    PM_MODEL_STATUS_BADARGS      = 0x08, ///< model fit called with invalid args
+    PM_MODEL_STATUS_LIMITS       = 0x10  ///< model parameters hit limits
+} pmModelStatus;
+
+typedef enum {
+    PM_MODEL_OP_NONE    = 0x00,
+    PM_MODEL_OP_FUNC    = 0x01,
+    PM_MODEL_OP_RES0    = 0x02,
+    PM_MODEL_OP_RES1    = 0x04,
+    PM_MODEL_OP_FULL    = 0x07,
+    PM_MODEL_OP_SKY     = 0x08,
+    PM_MODEL_OP_CENTER  = 0x10,
+    PM_MODEL_OP_NORM    = 0x20,
+    PM_MODEL_OP_NOISE   = 0x40,
+} pmModelOpMode;
+
+typedef struct pmModel pmModel;
+typedef struct pmSource pmSource;
+
+//  This function is the model chi-square minimization function for this model.
+typedef psMinimizeLMChi2Func pmModelFunc;
+
+//  This function sets the parameter limits for this model.
+typedef psMinimizeLMLimitFunc pmModelLimits;
+
+// This function returns the integrated flux for the given model parameters.
+typedef psF64 (*pmModelFlux)(const psVector *params);
+
+// This function returns the radius at which the given model and parameters
+// achieves the given flux.
+typedef psF64 (*pmModelRadius)(const psVector *params, double flux);
+
+//  This function provides the model guess parameters based on the details of
+//  the given source.
+typedef bool (*pmModelGuessFunc)(pmModel *model, pmSource *source);
+
+//  This function constructs the PSF model for the given source based on the
+//  supplied psf and the EXT model for the object.
+typedef bool (*pmModelFromPSFFunc)(pmModel *modelPSF, pmModel *modelEXT, pmPSF *psf);
+
+//  This function sets the model parameters based on the PSF for a given coordinate and central
+//  intensity
+typedef bool (*pmModelParamsFromPSF)(pmModel *model, pmPSF *psf, float Xo, float Yo, float Io);
+
+//  This function returns the success / failure status of the given model fit
+typedef bool (*pmModelFitStatusFunc)(pmModel *model);
+
+/** pmModel data structure
+ *
+ * Every source may have two types of models: a PSF model and a EXT (extended-source)
+ * model. The PSF model represents the best fit of the image PSF to the specific
+ * object. In this case, the PSF-dependent parameters are specified for the
+ * object by the PSF, not by the fit. The EXT model represents the best fit of
+ * the given model to the object, with all shape parameters floating in the fit.
+ *
+ */
+struct pmModel {
+    pmModelType type;                   ///< Model to be used.
+    psVector *params;                   ///< Paramater values.
+    psVector *dparams;                  ///< Parameter errors.
+    float chisq;                        ///< Fit chi-squared.
+    float chisqNorm;                    ///< re-normalized fit chi-squared.
+    int nDOF;                           ///< number of degrees of freedom
+    int nIter;                          ///< number of iterations to reach min
+    pmModelStatus flags;                ///< model status flags
+    float radiusFit;                    ///< fit radius actually used
+    pmResiduals *residuals;             ///< normalized PSF residuals
+
+    // functions for this model which depend on the model class
+    pmModelFunc          modelFunc;
+    pmModelFlux          modelFlux;
+    pmModelRadius        modelRadius;
+    pmModelLimits        modelLimits;
+    pmModelGuessFunc     modelGuess;
+    pmModelFromPSFFunc   modelFromPSF;
+    pmModelParamsFromPSF modelParamsFromPSF;
+    pmModelFitStatusFunc modelFitStatus;
+};
+
+/** Symbolic names for the elements of [d]params
+ * Note: these are #defines not enums as a given element of [d]params
+ * may/will correspond to different parameters in different contexts
+ */
+#define PM_PAR_SKY 0   ///< Sky
+#define PM_PAR_I0 1   ///< Central intensity
+#define PM_PAR_XPOS 2   ///< X centre of object
+#define PM_PAR_YPOS 3   ///< Y centre of object
+#define PM_PAR_SXX 4   ///< shape X^2 moment
+#define PM_PAR_SYY 5   ///< shape Y^2 moment
+#define PM_PAR_SXY 6   ///< shape XY moment
+#define PM_PAR_7 7   ///< ??? Unknown parameter
+#define PM_PAR_8 8   ///< ??? Unknown parameter
+
+/** pmModelAlloc()
+ *
+ */
+pmModel *pmModelAlloc(pmModelType type);
+
+// copy model to a new structure
+pmModel *pmModelCopy (pmModel *model);
+
+psF32 pmModelEval(pmModel *model, psImage *image, psS32 col, psS32 row);
+
+/** pmModelAdd()
+ *
+ * Add the given source model flux to/from the provided image. The boolean
+ * option center selects if the source is re-centered to the image center or if
+ * it is placed at its centroid location. The boolean option sky selects if the
+ * background sky is applied (TRUE) or not. The pixel range in the target image
+ * is at most the pixel range specified by the source.pixels image. The success
+ * status is returned.
+ *
+ */
+bool pmModelAdd(
+    psImage *image,                     ///< The output image (float)
+    psImage *mask,                      ///< The image pixel mask (valid == 0)
+    pmModel *model,                     ///< The input pmModel
+    pmModelOpMode mode,                 ///< mode to control how the model is added into the image
+    psMaskType maskVal                  ///< Value to mask
+);
+
+/** pmModelSub()
+ *
+ * Subtract the given source model flux to/from the provided image. The
+ * boolean option center selects if the source is re-centered to the image center
+ * or if it is placed at its centroid location. The boolean option sky selects if
+ * the background sky is applied (TRUE) or not. The pixel range in the target
+ * image is at most the pixel range specified by the source.pixels image. The
+ * success status is returned.
+ *
+ */
+bool pmModelSub(
+    psImage *image,                     ///< The output image (float)
+    psImage *mask,                      ///< The image pixel mask (valid == 0)
+    pmModel *model,                     ///< The input pmModel
+    pmModelOpMode mode,                 ///< mode to control how the model is added into the image
+    psMaskType maskVal                  ///< Value to mask
+);
+
+bool pmModelAddWithOffset(psImage *image,
+			  psImage *mask,
+			  pmModel *model,
+			  pmModelOpMode mode,
+			  psMaskType maskVal,
+			  int dx,
+			  int dy);
+
+bool pmModelSubWithOffset(psImage *image,
+			  psImage *mask,
+			  pmModel *model,
+			  pmModelOpMode mode,
+			  psMaskType maskVal,
+			  int dx,
+			  int dy);
+
+/** pmModelFitStatus()
+ *
+ * This function wraps the call to the model-specific function returned by
+ * pmModelFitStatusFunc_GetFunction.  The model-specific function examines the
+ * model parameters, parameter errors, Chisq, S/N, and other parameters available
+ * from model to decide if the particular fit was successful or not.
+ *
+ * XXX: Must code this.
+ *
+ */
+bool pmModelFitStatus(
+    pmModel *model                      ///< Model to be used
+);
+
+/// @}
+# endif /* PM_MODEL_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmModelClass.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmModelClass.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmModelClass.c	(revision 15358)
@@ -0,0 +1,142 @@
+/** @file  pmModelClass.c
+ *
+ *  Functions to define and manipulate object model attributes
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmErrorCodes.h"
+
+// XXX shouldn't these be defined for us in pslib.h ???
+double hypot(double x, double y);
+double sqrt (double x);
+
+# include "models/pmModel_GAUSS.c"
+# include "models/pmModel_PGAUSS.c"
+# include "models/pmModel_QGAUSS.c"
+# include "models/pmModel_RGAUSS.c"
+# include "models/pmModel_SERSIC.c"
+
+static pmModelClass defaultModels[] = {
+    {"PS_MODEL_GAUSS",        7, pmModelFunc_GAUSS,   pmModelFlux_GAUSS,   pmModelRadius_GAUSS,   pmModelLimits_GAUSS,   pmModelGuess_GAUSS,  pmModelFromPSF_GAUSS,  pmModelParamsFromPSF_GAUSS,  pmModelFitStatus_GAUSS},
+    {"PS_MODEL_PGAUSS",       7, pmModelFunc_PGAUSS,  pmModelFlux_PGAUSS,  pmModelRadius_PGAUSS,  pmModelLimits_PGAUSS,  pmModelGuess_PGAUSS, pmModelFromPSF_PGAUSS, pmModelParamsFromPSF_PGAUSS, pmModelFitStatus_PGAUSS},
+    {"PS_MODEL_QGAUSS",       8, pmModelFunc_QGAUSS,  pmModelFlux_QGAUSS,  pmModelRadius_QGAUSS,  pmModelLimits_QGAUSS,  pmModelGuess_QGAUSS, pmModelFromPSF_QGAUSS, pmModelParamsFromPSF_QGAUSS, pmModelFitStatus_QGAUSS},
+    {"PS_MODEL_RGAUSS",       8, pmModelFunc_RGAUSS,  pmModelFlux_RGAUSS,  pmModelRadius_RGAUSS,  pmModelLimits_RGAUSS,  pmModelGuess_RGAUSS, pmModelFromPSF_RGAUSS, pmModelParamsFromPSF_RGAUSS, pmModelFitStatus_RGAUSS},
+    {"PS_MODEL_SERSIC",       8, pmModelFunc_SERSIC,  pmModelFlux_SERSIC,  pmModelRadius_SERSIC,  pmModelLimits_SERSIC,  pmModelGuess_SERSIC, pmModelFromPSF_SERSIC, pmModelParamsFromPSF_SERSIC, pmModelFitStatus_SERSIC}
+};
+
+static pmModelClass *models = NULL;
+static int Nmodels = 0;
+
+static void ModelClassFree (pmModelClass *modelClass)
+{
+
+    if (modelClass == NULL)
+        return;
+    return;
+}
+
+pmModelClass *pmModelClassAlloc (int nModels)
+{
+    pmModelClass *modelClass = (pmModelClass *) psAlloc (nModels * sizeof(pmModelClass));
+    psMemSetDeallocator(modelClass, (psFreeFunc) ModelClassFree);
+    return (modelClass);
+}
+
+void pmModelClassAdd (pmModelClass *model)
+{
+    if (models == NULL) {
+        pmModelClassInit ();
+    }
+
+    Nmodels ++;
+    models = (pmModelClass *) psRealloc (models, Nmodels*sizeof(pmModelClass));
+    models[Nmodels-1] = model[0];
+    return;
+}
+
+bool pmModelClassInit (void)
+{
+    // if we do not need to init, return false;
+    if (models != NULL)
+        return false;
+
+    int Nnew = sizeof (defaultModels) / sizeof (pmModelClass);
+
+    models = pmModelClassAlloc (Nnew);
+    for (int i = 0; i < Nnew; i++) {
+        models[i] = defaultModels[i];
+    }
+    Nmodels = Nnew;
+    return true;
+}
+
+pmModelClass *pmModelClassSelect (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (&models[type]);
+}
+
+void pmModelClassCleanup (void)
+{
+    psFree (models);
+    models = NULL;
+    return;
+}
+
+psS32 pmModelClassParameterCount (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (0);
+    }
+    return (models[type].nParams);
+}
+
+psS32 pmModelClassGetType (char *name)
+{
+    for (int i = 0; i < Nmodels; i++) {
+        if (!strcmp(models[i].name, name)) {
+            return (i);
+        }
+    }
+    return (-1);
+}
+
+char *pmModelClassGetName (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (models[type].name);
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmModelClass.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmModelClass.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmModelClass.h	(revision 15358)
@@ -0,0 +1,73 @@
+/* @file  pmModelClass.h
+ *
+ * The object model function types are desined to allow for the flexible addition of new
+ * object models. Every object model, with parameters represented by pmModel, has an
+ * associated set of functions which provide necessary support operations.  A These
+ * functions allow the programmer to select the approriate function or property for a
+ * specific object model class.
+ *
+ * Every model instance belongs to a class of models, defined by the value of the
+ * pmModelType type entry. Various functions need access to information about each of the
+ * models. Some of this information varies from model to model, and may depend on the
+ * current parameter values or other data quantities. In order to keep the code from
+ * requiring the information about each model to be coded into the low-level fitting
+ * routines, we define a collection of functions which allow us to abstract this type of
+ * model-dependent information. These generic functions take the model type and return the
+ * corresponding function pointer for the specified model. Each model is defined by
+ * creating this collection of specific functions, and placing them in a single file for
+ * each model. We define the following structure to carry the collection of information
+ * about the models.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-08-24 00:11:02 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_MODEL_CLASS_H
+# define PM_MODEL_CLASS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef struct
+{
+    char *name;
+    int nParams;
+    pmModelFunc          modelFunc;
+    pmModelFlux          modelFlux;
+    pmModelRadius        modelRadius;
+    pmModelLimits        modelLimits;
+    pmModelGuessFunc     modelGuess;
+    pmModelFromPSFFunc   modelFromPSF;
+    pmModelParamsFromPSF modelParamsFromPSF;
+    pmModelFitStatusFunc modelFitStatus;
+} pmModelClass;
+
+// allocate a pmModelClass to hold nModels entries
+pmModelClass *pmModelClassAlloc (int nModels);
+
+// initialize the internal (static) model class with the default models
+bool pmModelClassInit (void);
+
+// free the internal (static) model class
+void pmModelClassCleanup (void);
+
+// add a new model class to the collection of model classes
+void pmModelClassAdd (pmModelClass *modelClass);
+
+// get the specified model class
+pmModelClass *pmModelClassSelect (pmModelType type);
+
+// This function returns the number of parameters used by the listed function.
+int pmModelClassParameterCount (pmModelType type);
+
+// This function returns the user-space model names for the specified model type.
+char *pmModelClassGetName (pmModelType type);
+
+// This function returns the internal model type code for the user-space model names.
+pmModelType pmModelClassGetType (char *name);
+
+/// @}
+# endif /* PM_MODEL_CLASS_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmModelGroup.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmModelGroup.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmModelGroup.c	(revision 15358)
@@ -0,0 +1,196 @@
+/** @file  pmModelGroup.c
+ *
+ *  Functions to define and manipulate object model attributes
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.18 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-08-24 00:11:02 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmModel.h"
+#include "pmModelGroup.h"
+#include "pmErrorCodes.h"
+
+// XXX shouldn't these be defined for us in pslib.h ???
+double hypot(double x, double y);
+double sqrt (double x);
+
+# include "models/pmModel_GAUSS.c"
+# include "models/pmModel_PGAUSS.c"
+# include "models/pmModel_QGAUSS.c"
+# include "models/pmModel_RGAUSS.c"
+# include "models/pmModel_SERSIC.c"
+
+static pmModelGroup defaultModels[] = {
+                                          {"PS_MODEL_GAUSS",        7, pmModelFunc_GAUSS,   pmModelFlux_GAUSS,   pmModelRadius_GAUSS,   pmModelLimits_GAUSS,   pmModelGuess_GAUSS,  pmModelFromPSF_GAUSS,  pmModelFitStatus_GAUSS},
+                                          {"PS_MODEL_PGAUSS",       7, pmModelFunc_PGAUSS,  pmModelFlux_PGAUSS,  pmModelRadius_PGAUSS,  pmModelLimits_PGAUSS,  pmModelGuess_PGAUSS, pmModelFromPSF_PGAUSS, pmModelFitStatus_PGAUSS},
+                                          {"PS_MODEL_QGAUSS",       8, pmModelFunc_QGAUSS,  pmModelFlux_QGAUSS,  pmModelRadius_QGAUSS,  pmModelLimits_QGAUSS,  pmModelGuess_QGAUSS, pmModelFromPSF_QGAUSS, pmModelFitStatus_QGAUSS},
+                                          {"PS_MODEL_RGAUSS",       8, pmModelFunc_RGAUSS,  pmModelFlux_RGAUSS,  pmModelRadius_RGAUSS,  pmModelLimits_RGAUSS,  pmModelGuess_RGAUSS, pmModelFromPSF_RGAUSS, pmModelFitStatus_RGAUSS},
+                                          {"PS_MODEL_SERSIC",       8, pmModelFunc_SERSIC,  pmModelFlux_SERSIC,  pmModelRadius_SERSIC,  pmModelLimits_SERSIC,  pmModelGuess_SERSIC, pmModelFromPSF_SERSIC, pmModelFitStatus_SERSIC}
+                                      };
+
+static pmModelGroup *models = NULL;
+static int Nmodels = 0;
+
+static void ModelGroupFree (pmModelGroup *modelGroup)
+{
+
+    if (modelGroup == NULL)
+        return;
+    return;
+}
+
+pmModelGroup *pmModelGroupAlloc (int nModels)
+{
+
+    pmModelGroup *modelGroup = (pmModelGroup *) psAlloc (nModels * sizeof(pmModelGroup));
+    psMemSetDeallocator(modelGroup, (psFreeFunc) ModelGroupFree);
+    return (modelGroup);
+}
+
+void pmModelGroupAdd (pmModelGroup *model)
+{
+
+    if (models == NULL) {
+        pmModelGroupInit ();
+    }
+
+    Nmodels ++;
+    models = (pmModelGroup *) psRealloc (models, Nmodels*sizeof(pmModelGroup));
+    models[Nmodels-1] = model[0];
+    return;
+}
+
+bool pmModelGroupInit (void)
+{
+
+    // if we do not need to init, return false;
+    if (models != NULL)
+        return false;
+
+    int Nnew = sizeof (defaultModels) / sizeof (pmModelGroup);
+
+    models = pmModelGroupAlloc (Nnew);
+    for (int i = 0; i < Nnew; i++) {
+        models[i] = defaultModels[i];
+    }
+    Nmodels = Nnew;
+    return true;
+}
+
+void pmModelGroupCleanup (void)
+{
+    psFree (models);
+    models = NULL;
+    return;
+}
+
+pmModelFunc pmModelFunc_GetFunction (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (models[type].modelFunc);
+}
+
+pmModelFlux pmModelFlux_GetFunction (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (models[type].modelFlux);
+}
+
+pmModelRadius pmModelRadius_GetFunction (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (models[type].modelRadius);
+}
+
+pmModelLimits pmModelLimits_GetFunction (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (models[type].modelLimits);
+}
+
+pmModelGuessFunc pmModelGuessFunc_GetFunction (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (models[type].modelGuessFunc);
+}
+
+pmModelFitStatusFunc pmModelFitStatusFunc_GetFunction (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (models[type].modelFitStatusFunc);
+}
+
+pmModelFromPSFFunc pmModelFromPSFFunc_GetFunction (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (models[type].modelFromPSFFunc);
+}
+
+psS32 pmModelParameterCount (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (0);
+    }
+    return (models[type].nParams);
+}
+
+psS32 pmModelSetType (char *name)
+{
+    for (int i = 0; i < Nmodels; i++) {
+        if (!strcmp(models[i].name, name)) {
+            return (i);
+        }
+    }
+    return (-1);
+}
+
+char *pmModelGetType (pmModelType type)
+{
+    if ((type < 0) || (type >= Nmodels)) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return (NULL);
+    }
+    return (models[type].name);
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmModelGroup.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmModelGroup.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmModelGroup.h	(revision 15358)
@@ -0,0 +1,199 @@
+/* @file  pmModelGroup.h
+ *
+ * The object model function types are desined to allow for the flexible addition of new object
+ * models. Every object model, with parameters represented by pmModel, has an associated set of
+ * functions which provide necessary support operations. A set of abstract functions allow the
+ * programmer to select the approriate function or property for a specific named object model.
+ *
+ * Every model instance belongs to a class of models, defined by the value of
+ * the pmModelType type entry. Various functions need access to information about
+ * each of the models. Some of this information varies from model to model, and
+ * may depend on the current parameter values or other data quantities. In order
+ * to keep the code from requiring the information about each model to be coded
+ * into the low-level fitting routines, we define a collection of functions which
+ * allow us to abstract this type of model-dependent information. These generic
+ * functions take the model type and return the corresponding function pointer
+ * for the specified model. Each model is defined by creating this collection of
+ * specific functions, and placing them in a single file for each model. We
+ * define the following structure to carry the collection of information about
+ * the models.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.8 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-08-24 00:11:02 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_MODEL_GROUP_H
+# define PM_MODEL_GROUP_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+//  This function is the model chi-square minimization function for this model.
+typedef psMinimizeLMChi2Func pmModelFunc;
+
+//  This function sets the parameter limits for this model.
+typedef psMinimizeLMLimitFunc pmModelLimits;
+
+// This function returns the integrated flux for the given model parameters.
+typedef psF64 (*pmModelFlux)(const psVector *params);
+
+// This function returns the radius at which the given model and parameters
+// achieves the given flux.
+typedef psF64 (*pmModelRadius)(const psVector *params, double flux);
+
+//  This function provides the model guess parameters based on the details of
+//  the given source.
+typedef bool (*pmModelGuessFunc)(pmModel *model, pmSource *source);
+
+//  This function constructs the PSF model for the given source based on the
+//  supplied psf and the EXT model for the object.
+typedef bool (*pmModelFromPSFFunc)(pmModel *modelPSF, pmModel *modelEXT, pmPSF *psf);
+
+//  This function sets the model parameters based on the PSF for a given coordinate and central
+//  intensity
+typedef bool (*pmModelParamsFromPSF)(pmModel *model, pmPSF *psf, float Xo, float Yo, float Io);
+
+//  This function returns the success / failure status of the given model fit
+typedef bool (*pmModelFitStatusFunc)(pmModel *model);
+
+typedef struct
+{
+    char *name;
+    int nParams;
+    pmModelFunc          modelFunc;
+    pmModelFlux          modelFlux;
+    pmModelRadius        modelRadius;
+    pmModelLimits        modelLimits;
+    pmModelGuessFunc     modelGuessFunc;
+    pmModelFromPSFFunc   modelFromPSFFunc;
+    pmModelParamsFromPSF modelParamsFromPSF;
+    pmModelFitStatusFunc modelFitStatusFunc;
+}
+pmModelGroup;
+
+// allocate a pmModelGroup to hold nModels entries
+pmModelGroup *pmModelGroupAlloc (int nModels);
+
+// initialize the internal (static) model group with the default models
+bool pmModelGroupInit (void);
+
+// free the internal (static) model group
+void pmModelGroupCleanup (void);
+
+// add a new model to the internal (static) model group
+void pmModelGroupAdd (pmModelGroup *model);
+
+/* This function returns the number of parameters used by the listed function.
+ */
+int pmModelParameterCount(
+    pmModelType type                    ///< Add comment.
+);
+
+
+/* This function returns the user-space model names for the specified model type.
+ */
+char *pmModelGetType(
+    pmModelType type                    ///< Add comment.
+);
+
+
+/**
+ * 
+ * This function returns the internal model type code for the user-space model names.
+ * 
+ */
+pmModelType pmModelSetType(
+    char *name                          ///< Add comment.
+);
+
+/**
+ * 
+ *  Each of the function types above has a corresponding function which returns
+ *  the function given the model type:
+ * 
+ */
+
+/**
+ * 
+ * pmModelFunc is the function used to determine the value of the model at a
+ * specific coordinate, and is the one used by psMinimizeLMChi2.
+ * 
+ */
+pmModelFunc          pmModelFunc_GetFunction (pmModelType type);
+
+
+/**
+ * 
+ * pmModelFlux returns the total integrated flux for the given input parameters.
+ * 
+ */
+pmModelFlux          pmModelFlux_GetFunction (pmModelType type);
+
+
+/**
+ * 
+ * pmModelRadius returns the scaling radius at which the flux of the model
+ * matches the specified flux. This presumes that the model is a function of an
+ * elliptical contour.
+ * 
+ */
+pmModelRadius        pmModelRadius_GetFunction (pmModelType type);
+
+
+/**
+ * 
+ * pmModelLimits sets the parameter limit vectors for the function.
+ * 
+ */
+pmModelLimits        pmModelLimits_GetFunction (pmModelType type);
+
+
+/**
+ * 
+ * pmModelGuessFunc generates an initial guess for the model based on the
+ * provided source statistics (moments and pixel values as needed).
+ * 
+ */
+pmModelGuessFunc     pmModelGuessFunc_GetFunction (pmModelType type);
+
+
+/**
+ * 
+ * pmModelFromPSFFunc takes as input a representation of the psf and a value
+ * for the model and fills in the PSF parameters of the model. The input
+ * primarily relies upon the centroid coordinates of the input model, though the
+ * normalization may potentially be used.
+ * 
+ */
+pmModelFromPSFFunc   pmModelFromPSFFunc_GetFunction (pmModelType type);
+
+
+/**
+ * 
+ * pmModelFitStatusFunc returns a true or false values based on the success or
+ * failure of a model fit.  The success is determined by quantities such as the
+ * chisq or the signal-to-noise.
+ * 
+ */
+pmModelFitStatusFunc pmModelFitStatusFunc_GetFunction (pmModelType type);
+
+
+/** pmSourceModelGuess()
+ *
+ * Convert available data to an initial guess for the given model. This
+ * function allocates a pmModel entry for the pmSource structure based on the
+ * provided model selection. The method of defining the model parameter guesses
+ * are specified for each model below. The guess values are placed in the model
+ * parameters. The function returns TRUE on success or FALSE on failure.
+ *
+ */
+pmModel *pmSourceModelGuess(
+    pmSource *source,   ///< The input pmSource
+    pmModelType model   ///< The type of model to be created.
+);
+
+/// @}
+# endif /* PM_MODEL_GROUP_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmModelUtils.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmModelUtils.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmModelUtils.c	(revision 15358)
@@ -0,0 +1,90 @@
+/** @file  pmModelUtils.c
+ *
+ *  Functions to manipulate object models
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:04:30 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmErrorCodes.h"
+#include "pmModelUtils.h"
+
+/*****************************************************************************
+pmModelFromPSF (*modelEXT, *psf):  use the model position parameters to
+construct a realization of the PSF model at the object coordinates
+ *****************************************************************************/
+pmModel *pmModelFromPSF (pmModel *modelEXT, pmPSF *psf)
+{
+    // allocate a new pmModel to hold the PSF version
+    pmModel *modelPSF = pmModelAlloc (psf->type);
+
+    // set model parameters for this source based on PSF information
+    if (!modelEXT->modelFromPSF (modelPSF, modelEXT, psf)) {
+        psError(PM_ERR_PSF, false, "Failed to set model params from PSF");
+        psFree(modelPSF);
+        return NULL;
+    }
+    // XXX note that model->residuals is just a reference
+    modelPSF->residuals = psf->residuals;
+
+    return (modelPSF);
+}
+
+// instantiate a model for the PSF at this location with peak flux
+// NOTE: psf and (Xo,Yo) are defined wrt chip coordinates
+pmModel *pmModelFromPSFforXY (pmPSF *psf, float Xo, float Yo, float Io) {
+
+    assert (psf);
+
+    // allocate a new pmModel to hold the PSF version
+    pmModel *modelPSF = pmModelAlloc (psf->type);
+
+    // set model parameters for this source based on PSF information
+    if (!modelPSF->modelParamsFromPSF (modelPSF, psf, Xo, Yo, Io)) {
+        psError(PM_ERR_PSF, false, "Failed to set model params from PSF");
+        psFree(modelPSF);
+        return NULL;
+    }
+
+    // XXX note that model->residuals is just a reference
+    modelPSF->residuals = psf->residuals;
+
+    return (modelPSF);
+}
+
+// set this model to have the requested flux
+bool pmModelSetFlux (pmModel *model, float flux) {
+
+    // set Io to be 1.0
+    model->params->data.F32[PM_PAR_I0] = 1.0;
+
+    // determine the normalized flux
+    float normFlux = model->modelFlux (model->params);
+    assert (isfinite(normFlux));
+    assert (normFlux > 0);
+
+    // set the desired normalization
+    model->params->data.F32[PM_PAR_I0] = flux / normFlux;
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmModelUtils.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmModelUtils.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmModelUtils.h	(revision 15358)
@@ -0,0 +1,47 @@
+/* @file  pmModelUtils.h
+ *
+ * Utility functions for working with pmSources
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-08-24 00:11:02 $
+ * Copyright 2007 IfA, University of Hawaii
+ */
+
+# ifndef PM_MODEL_UTILS_H
+# define PM_MODEL_UTILS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/**
+ *
+ * This function constructs a pmModel instance based on the pmPSF description
+ * of the PSF. The input is a pmModel with at least the values of the centroid
+ * coordinates (possibly normalization if this is needed) defined. The values of
+ * the PSF-dependent parameters are specified for the specific realization based
+ * on the coordinates of the object.
+ *
+ */
+pmModel *pmModelFromPSF(
+    pmModel *model,                     ///< Add comment
+    pmPSF *psf                          ///< Add comment
+);
+
+pmModel *pmModelFromPSFforXY (
+    pmPSF *psf, 
+    float Xo, 
+    float Yo, 
+    float Io
+    );
+
+bool pmModelSetFlux (
+    pmModel *model, 
+    float flux
+    );
+
+
+
+/// @}
+# endif /* PM_MODEL_UTILS_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmMoments.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmMoments.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmMoments.c	(revision 15358)
@@ -0,0 +1,44 @@
+/** @file  pmMoments.c
+ *
+ *  Functions defining the pmMoments structure
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-09-15 09:49:01 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+#include "pmMoments.h"
+
+/******************************************************************************
+pmMomentsAlloc(): Allocate the pmMoments structure and initialize the members
+to zero.
+*****************************************************************************/
+pmMoments *pmMomentsAlloc()
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    pmMoments *tmp = (pmMoments *) psAlloc(sizeof(pmMoments));
+    tmp->x = 0.0;
+    tmp->y = 0.0;
+    tmp->Sx = 0.0;
+    tmp->Sy = 0.0;
+    tmp->Sxy = 0.0;
+    tmp->Sum = 0.0;
+    tmp->Peak = 0.0;
+    tmp->Sky = 0.0;
+    tmp->nPixels = 0;
+
+    psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+    return(tmp);
+}
+
Index: /branches/eam_branch_20071023/psModules/src/objects/pmMoments.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmMoments.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmMoments.h	(revision 15358)
@@ -0,0 +1,46 @@
+/* @file  pmMoments.h
+ * @brief Definitions of the moments structure
+ *
+ * @author GLG, MHPCC
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:15 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_MOMENTS_H
+# define PM_MOMENTS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/** pmMoments data structure
+ *
+ * One of the simplest measurements which can be made quickly for an object
+ * are the object moments. We specify a structure to carry the moment information
+ * for a specific source:
+ *
+ */
+typedef struct
+{
+    float x;     ///< X-coord of centroid.
+    float y;     ///< Y-coord of centroid.
+    float Sx;    ///< x-second moment.
+    float Sy;    ///< y-second moment.
+    float Sxy;   ///< xy cross moment.
+    float Sum;   ///< Pixel sum above sky (background).
+    float Peak;  ///< Peak counts above sky.
+    float Sky;   ///< Sky level (background).
+    float dSky;  ///< local Sky variance
+    float SN;    ///< approx signal-to-noise
+    int nPixels; ///< Number of pixels used.
+}
+pmMoments;
+
+/** pmMomentsAlloc()
+ *
+ */
+pmMoments *pmMomentsAlloc();
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/objects/pmObjects.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmObjects.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmObjects.c	(revision 15358)
@@ -0,0 +1,25 @@
+/** @file  pmObjects.c
+ *
+ *  This file will ...
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-09-15 09:49:01 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmObjects.h"
+#include "pmModelGroup.h"
+
Index: /branches/eam_branch_20071023/psModules/src/objects/pmObjects.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmObjects.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmObjects.h	(revision 15358)
@@ -0,0 +1,68 @@
+/* @file  pmObjects.h
+ *
+ * The process of finding, measuring, and classifying astronomical sources on
+ * images is one of the critical tasks of the IPP or any astronomical software
+ * system. This file will define structures and functions related to the task
+ * of source detection and measurement. The elements defined in this section
+ * are generally low-level components which can be connected together to
+ * construct a complete object measurement suite.
+ *
+ * @author GLG, MHPCC
+ *
+ * @version $Revision: 1.8 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:15 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#ifndef PM_OBJECTS_H
+#define PM_OBJECTS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+#include <stdio.h>
+#include <math.h>
+#include <pslib.h>
+
+/**
+ *
+ * This function converts the source classification into the closest available
+ * approximation to the Dophot classification scheme:
+ * XXX EAM : fix this to use current source classification scheme
+ *
+ * PM_SOURCE_DEFECT: 8
+ * PM_SOURCE_SATURATED: 8
+ * PM_SOURCE_SATSTAR: 10
+ * PM_SOURCE_PSFSTAR: 1
+ * PM_SOURCE_GOODSTAR: 1
+ * PM_SOURCE_POOR_FIT_PSF: 7
+ * PM_SOURCE_FAIL_FIT_PSF: 4
+ * PM_SOURCE_FAINTSTAR: 4
+ * PM_SOURCE_GALAXY: 2
+ * PM_SOURCE_FAINT_GALAXY: 2
+ * PM_SOURCE_DROP_GALAXY: 2
+ * PM_SOURCE_FAIL_FIT_GAL: 2
+ * PM_SOURCE_POOR_FIT_GAL: 2
+ * PM_SOURCE_OTHER: ?
+ *
+ */
+int pmSourceDophotType(
+    pmSource *source                    ///< Add comment.
+);
+
+
+/** pmSourceSextractType()
+ *
+ * This function converts the source classification into the closest available
+ * approximation to the Sextractor classification scheme. the correspondence is
+ * not yet defined (TBD) .
+ *
+ * XXX: Must code this.
+ *
+ */
+int pmSourceSextractType(
+    pmSource *source                    ///< Add comment.
+);
+
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/src/objects/pmPSF.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmPSF.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmPSF.c	(revision 15358)
@@ -0,0 +1,470 @@
+/** @file  pmPSF.c
+ *
+ * This file contains typedefs for the Point-Spread Function and prototypes
+ * for functions that calculate the PSF.
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.32 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-29 00:15:32 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+/*****************************************************************************/
+/* INCLUDE FILES                                                             */
+/*****************************************************************************/
+
+#include <strings.h>  // for strcasecmp
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmModelUtils.h"
+#include "pmSourcePhotometry.h"
+#include "pmFPAMaskWeight.h"
+#include "psVectorBracket.h"
+#include "pmErrorCodes.h"
+
+/*****************************************************************************/
+/* FUNCTION IMPLEMENTATION - PUBLIC                                          */
+/*****************************************************************************/
+
+static void pmPSFOptionsFree (pmPSFOptions *options) {
+
+    if (!options) return;
+
+    psFree (options->stats);
+    return;
+}
+
+pmPSFOptions *pmPSFOptionsAlloc () {
+
+    pmPSFOptions *options = (pmPSFOptions *) psAlloc(sizeof(pmPSFOptions));
+    psMemSetDeallocator(options, (psFreeFunc) pmPSFOptionsFree);
+
+    options->type          = 0;
+
+    options->stats         = NULL;
+
+    options->psfTrendMode  = PM_TREND_NONE;
+    options->psfTrendNx    = 0;
+    options->psfTrendNy    = 0;
+    options->psfFieldNx    = 0;
+    options->psfFieldNy    = 0;
+    options->psfFieldXo    = 0;
+    options->psfFieldYo    = 0;
+
+    options->poissonErrorsPhotLMM = true;
+    options->poissonErrorsPhotLin = false;
+    options->poissonErrorsParams  = true;
+
+    return options;
+}
+
+/*****************************************************************************
+pmPSFFree(psf): function to free a pmPSF structure
+ *****************************************************************************/
+static void pmPSFFree (pmPSF *psf)
+{
+
+    if (psf == NULL)
+        return;
+
+    psFree (psf->ChiTrend);
+    psFree (psf->psfTrendStats);
+    psFree (psf->ApTrend);
+    psFree (psf->FluxScale);
+    psFree (psf->growth);
+    psFree (psf->params);
+    psFree (psf->residuals);
+    return;
+}
+
+/*****************************************************************************
+ pmPSFAlloc (type): allocate a pmPSF.
+
+ NOTE: PSF model parameters which are not modeled on an image are set to NULL in psf->params.
+
+ These are normally:
+
+ X-center
+ Y-center
+ Sky background value
+ Object Normalization
+ *****************************************************************************/
+pmPSF *pmPSFAlloc (pmPSFOptions *options)
+{
+    int Nparams;
+
+    pmPSF *psf = (pmPSF *) psAlloc(sizeof(pmPSF));
+    psMemSetDeallocator(psf, (psFreeFunc) pmPSFFree);
+
+    psf->type     = options->type;
+    psf->chisq    = 0.0;
+    psf->ApResid  = 0.0;
+    psf->dApResid = 0.0;
+    psf->skyBias  = 0.0;
+    psf->skySat   = 0.0;
+    psf->nPSFstars  = 0;
+    psf->nApResid   = 0;
+
+    psf->poissonErrorsPhotLMM = options->poissonErrorsPhotLMM;
+    psf->poissonErrorsPhotLin = options->poissonErrorsPhotLin;
+    psf->poissonErrorsParams = options->poissonErrorsParams;
+
+    // the ApTrend components are (x, y).  It may be represented with a polynomial or with a
+    // psImageMap.  We set it initially to NULL. the user must allocate it before using.
+    // psf->ApTrend = pmTrend2DAlloc (PM_TREND_MAP, Nx, Ny, 1, 1, stats);
+    psf->ApTrend = NULL;
+
+    // the flux scale is the relationship between the integrated flux and the peak flux for a
+    // psf model.  this is a 2D function of position, and is modeled with pmTrend2D after the
+    // psf model is determined for an image.  until it is determined, the flux calculation
+    // integrates the sources
+    psf->FluxScale = NULL;
+
+    if (psf->poissonErrorsPhotLMM) {
+        psf->ChiTrend = psPolynomial1DAlloc (PS_POLYNOMIAL_ORD, 1);
+    } else {
+        psf->ChiTrend = psPolynomial1DAlloc (PS_POLYNOMIAL_ORD, 2);
+    }
+
+    // don't define a growth curve : user needs to choose radius bins
+    psf->growth = NULL;
+
+    // by default, we do not construct the residual image
+    psf->residuals = NULL;
+
+    Nparams = pmModelClassParameterCount (options->type);
+    if (!Nparams) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return(NULL);
+    }
+    psf->params = psArrayAlloc(Nparams);
+    
+    // save the trend stats on the psf for use in pmPSFFromPSFtry
+    psf->psfTrendStats = psMemIncrRefCounter (options->stats);
+
+    // the psf parameters may have 2D variations represented as either a polynomial (ordinary
+    // or chebychev) or as an image map.  The size of the image map is determined by pmPSFtry
+    // by minimizing the scatter in the fitted data and the rms error in the complete image
+    // map.  In this case, the user-supplied options of psfTrendNx and psfTrendNy are the
+    // maximum value used for these axes.  For the polynomial terms, the order is not currently
+    // set, dynamically.  The value of psfTrendNx and psfTrendNy are used.
+
+    psImageBinning *binning = psImageBinningAlloc();
+    binning->nXruff = options->psfTrendNx;
+    binning->nYruff = options->psfTrendNy;
+    binning->nXfine = options->psfFieldNx;
+    binning->nYfine = options->psfFieldNy;
+
+    // for polynomial representations, nXruff, nYruff are the order number, and may be 0.
+    // in this case, we cannot set the psImageBinning scale because 0 would be invalid.  these
+    // elements are only used by the image map representation
+    if (options->psfTrendMode == PM_TREND_MAP) {
+	psImageBinningSetScale (binning, PS_IMAGE_BINNING_CENTER);
+	psImageBinningSetSkipByOffset (binning, options->psfFieldXo, options->psfFieldYo);
+    }
+
+    // trendNx & trendNy are used in pmPSFtry as the max for these values
+    psf->psfTrendMode = options->psfTrendMode;
+    psf->trendNx      = options->psfTrendNx;
+    psf->trendNy      = options->psfTrendNy;
+    psf->fieldNx      = options->psfFieldNx;
+    psf->fieldNy      = options->psfFieldNy;
+    psf->fieldXo      = options->psfFieldXo;
+    psf->fieldYo      = options->psfFieldYo;
+
+    // define the parameter trends
+    if (options->psfTrendMode != PM_TREND_NONE) {
+        for (int i = 0; i < psf->params->n; i++) {
+            if (i == PM_PAR_SKY) continue;
+            if (i == PM_PAR_I0) continue;
+            if (i == PM_PAR_XPOS) continue;
+            if (i == PM_PAR_YPOS) continue;
+
+            psf->params->data[i] = pmTrend2DNoImageAlloc (options->psfTrendMode, binning, options->stats);
+        }
+    }
+    psFree (binning);
+    return psf;
+}
+
+// the PSF models the \sigma_{xy} variation of the elliptical contour as a function of position in the image with a
+// polynomial.  an individual object has a contour of the form (x^2/2sx^2) + (y^2/2sy^2) + sxy*x*y
+// these are the values of the model->params.  the psf->params term for sxy is actually fitted
+// to sxy/(sxx^-2 + syy^-2)^2
+
+// XXX this is only an approximate solution.  A better solution would be to fit the second moment, Mxy. the
+// problem here is that converting from Mxy,SXX,SYY -> SXY is a third order problem:
+// Mxy = SXY * (SXX^-4 + SYY^-4 - 2 SXY ^2)
+
+// input: model->param, output: psf->param[PM_PAR_SXY]
+double pmPSF_SXYfromModel (psF32 *modelPar)
+{
+
+    double SXX = modelPar[PM_PAR_SXX];
+    double SYY = modelPar[PM_PAR_SYY];
+    double SXY = modelPar[PM_PAR_SXY];
+
+    double par = SXY / PS_SQR(1.0 / PS_SQR(SXX) + 1.0 / PS_SQR(SYY));
+    return (par);
+}
+
+// input: fitted psf->param, output: model->param[PM_PAR_SXY]
+double pmPSF_SXYtoModel (psF32 *fittedPar)
+{
+
+    double SXX = fittedPar[PM_PAR_SXX];
+    double SYY = fittedPar[PM_PAR_SYY];
+    double fit = fittedPar[PM_PAR_SXY];
+
+    double SXY = fit * PS_SQR(1.0 / PS_SQR(SXX) + 1.0 / PS_SQR(SYY));
+
+    assert (!isnan(SXY));
+
+    return SXY;
+}
+
+// New Concept: the PSF modelling function fits the polarization terms e0, e1, e2:
+
+// convert the parameters used in the fitted source model
+// to the parameters used in the 2D PSF model
+bool pmPSF_FitToModel (psF32 *fittedPar, float minMinorAxis)
+{
+    psEllipsePol pol;
+
+    pol.e0 = fittedPar[PM_PAR_E0];
+    pol.e1 = fittedPar[PM_PAR_E1];
+    pol.e2 = fittedPar[PM_PAR_E2];
+
+    psEllipseAxes axes;
+    if (psEllipsePolToAxes (pol, minMinorAxis, &axes) != PS_ERR_NONE) {
+        psError(PM_ERR_PSF, false, "Failed to convert e[012] (%g,%g,%g) to axes",
+                pol.e0, pol.e1, pol.e2);
+        return false;
+    }
+    psEllipseShape shape = psEllipseAxesToShape (axes);
+
+    fittedPar[PM_PAR_SXX] = shape.sx * M_SQRT2;
+    fittedPar[PM_PAR_SYY] = shape.sy * M_SQRT2;
+    fittedPar[PM_PAR_SXY] = shape.sxy;
+
+    return true;
+}
+
+// convert the PSF parameters used in the 2D PSF model fit into the
+// parameters used in the source model
+psEllipsePol pmPSF_ModelToFit (psF32 *modelPar)
+{
+    psEllipseShape shape;
+
+    shape.sx  = modelPar[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = modelPar[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = modelPar[PM_PAR_SXY];
+
+    psEllipsePol pol = psEllipseShapeToPol (shape);
+
+    return pol;
+}
+
+// convert the parameters used in the fitted source model to the psEllipseAxes representation
+// (major,minor,theta)
+psEllipseAxes pmPSF_ModelToAxes (psF32 *modelPar, double maxAR)
+{
+    psEllipseShape shape;
+    psEllipseAxes axes;
+
+    shape.sx  = modelPar[PM_PAR_SXX] / M_SQRT2;
+    shape.sy  = modelPar[PM_PAR_SYY] / M_SQRT2;
+    shape.sxy = modelPar[PM_PAR_SXY];
+
+    if ((shape.sx == 0) || (shape.sy == 0)) {
+        axes.major = 0.0;
+        axes.minor = 0.0;
+        axes.theta = 0.0;
+    } else {
+        // XXX this is not really consistent with the model fit range above
+        axes = psEllipseShapeToAxes (shape, maxAR);
+    }
+
+    return axes;
+}
+
+// convert the psEllipseAxes representation (major,minor,theta) to the parameters used in the
+// fitted source model
+bool pmPSF_AxesToModel (psF32 *modelPar, psEllipseAxes axes)
+{
+    if ((axes.major <= 0) || (axes.minor <= 0)) {
+        modelPar[PM_PAR_SXX] = 0.0;
+        modelPar[PM_PAR_SYY] = 0.0;
+        modelPar[PM_PAR_SXY] = 0.0;
+        return true;
+    }
+
+    psEllipseShape shape = psEllipseAxesToShape (axes);
+
+    modelPar[PM_PAR_SXX] = shape.sx * M_SQRT2;
+    modelPar[PM_PAR_SYY] = shape.sy * M_SQRT2;
+    modelPar[PM_PAR_SXY] = shape.sxy;
+
+    return true;
+}
+
+// generate a psf model of the requested type, with fixed shape
+pmPSF *pmPSFBuildSimple (char *typeName, float sxx, float syy, float sxy, ...)
+{
+
+    va_list ap;
+    va_start(ap, sxy);
+
+    pmPSFOptions *options = pmPSFOptionsAlloc ();
+    options->type = pmModelClassGetType (typeName);
+    options->psfTrendMode = PM_TREND_POLY_ORD;
+    options->psfTrendNx = 0;
+    options->psfTrendNy = 0;
+
+    pmPSF *psf = pmPSFAlloc (options);
+
+    psVector *par = psVectorAlloc (psf->params->n, PS_TYPE_F32);
+    par->data.F32[PM_PAR_SXX] = sxx;
+    par->data.F32[PM_PAR_SYY] = syy;
+    par->data.F32[PM_PAR_SXY] = sxy;
+
+    psEllipsePol pol = pmPSF_ModelToFit(par->data.F32);
+
+    pmTrend2D *trend = NULL;
+
+    // set the psf shape parameters
+    trend = psf->params->data[PM_PAR_E0];
+    trend->poly->coeff[0][0] = pol.e0;
+
+    trend = psf->params->data[PM_PAR_E1];
+    trend->poly->coeff[0][0] = pol.e1;
+
+    trend = psf->params->data[PM_PAR_E2];
+    trend->poly->coeff[0][0] = pol.e2;
+
+    for (int i = PM_PAR_SXY + 1; i < psf->params->n; i++) {
+	trend = psf->params->data[i];
+        trend->poly->coeff[0][0] = (psF32)va_arg(ap, psF64);
+    }
+    va_end(ap);
+
+    psFree (par);
+    psFree (options);
+    return psf;
+}
+
+// we generate the growth curve for the center of the image with the specified psf model
+bool pmGrowthCurveGenerate (pmReadout *readout, pmPSF *psf, bool ignore, psMaskType maskVal, psMaskType mark)
+{
+
+    // bool status;
+    float xc, yc, dx, dy;
+    float fitMag, apMag;
+    float radius;
+
+    // create template model
+    pmModel *modelRef = pmModelAlloc(psf->type);
+
+    // use the center of the center pixel of the image
+    xc = 0.5*readout->image->numCols + readout->image->col0 + 0.5;
+    yc = 0.5*readout->image->numRows + readout->image->row0 + 0.5;
+    dx = psf->growth->maxRadius + 1;
+    dy = psf->growth->maxRadius + 1;
+
+    // assign the x and y coords to the image center
+    // create an object with center intensity of 1000
+    modelRef->params->data.F32[PM_PAR_SKY] = 0;
+    modelRef->params->data.F32[PM_PAR_I0] = 1000;
+    modelRef->params->data.F32[PM_PAR_XPOS] = xc;
+    modelRef->params->data.F32[PM_PAR_YPOS] = yc;
+
+    // create modelPSF from this model
+    pmModel *model = pmModelFromPSF (modelRef, psf);
+
+    // measure the fitMag for this model
+    pmSourcePhotometryModel (&fitMag, model);
+    psf->growth->fitMag = fitMag;
+
+    // generate working image for this source
+    psRegion region = {xc - dx, xc + dx, yc - dy, yc + dy};
+
+    // force region to stop at dimensions of image
+    region = psRegionForImage (readout->image, region);
+
+    // the view, image, and mask retain col0,row0
+    psImage *view = psImageSubset (readout->image, region);
+    psImage *image = psImageCopy (NULL, view, PS_TYPE_F32);
+    psImage *mask = psImageCopy (NULL, view, PS_TYPE_U8);
+
+    psImageInit (image, 0.0);
+    psImageInit (mask, 0);
+
+    // place the reference object in the image center
+    // no need to mask the source here
+    // XXX should we measure this for the analytical model only or the full model?
+    pmModelAdd (image, NULL, model, PM_MODEL_OP_FULL, maskVal);
+
+    // loop over a range of source fluxes
+    // no need to interpolate since we have forced the object center
+    // to 0.5, 0.5 above
+    bool completeGrowthCurve = true;            // do we have a complete curve of growth?
+    for (int i = 0; i < psf->growth->radius->n; i++) {
+
+        radius = psf->growth->radius->data.F32[i];
+
+        // mask the given aperture and measure the apMag
+        psImageKeepCircle (mask, xc, yc, radius, "OR", mark);
+        if (!pmSourcePhotometryAper (&apMag, model, image, mask, maskVal)) {
+            psError(PM_ERR_PHOTOM, false, "Measuring apMag for radius == %g", radius);
+	    psErrorStackPrint (stderr, "failure to get growth curve");
+	    psErrorClear();
+            completeGrowthCurve = false;
+            break;
+        }
+
+        // XXX since we re-mask on each pass, this could be dropped.
+        psImageKeepCircle (mask, xc, yc, radius, "AND", PS_NOT_U8(mark));
+
+        // the 'ignore' mode is for testing
+        if (ignore) {
+            psf->growth->apMag->data.F32[i] = fitMag;
+        } else {
+            psf->growth->apMag->data.F32[i] = apMag;
+        }
+    }
+
+    if (completeGrowthCurve) {
+        psf->growth->apRef = psVectorInterpolate (psf->growth->radius, psf->growth->apMag, psf->growth->refRadius);
+        psf->growth->apLoss = psf->growth->fitMag - psf->growth->apRef;
+    } else {
+        psf->growth->apRef = NAN;
+        psf->growth->apLoss = 0;
+    }
+
+    psLogMsg ("psphot.growth", 4, "GrowthCurve : apLoss : %f\n", psf->growth->apLoss);
+
+    psFree (view);
+    psFree (image);
+    psFree (mask);
+    psFree (model);
+    psFree (modelRef);
+
+    return completeGrowthCurve ? true : false;
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmPSF.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmPSF.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmPSF.h	(revision 15358)
@@ -0,0 +1,105 @@
+/* @file  pmPSF.h
+ *
+ * This file contains typedefs for the Point-Spread Function and prototypes
+ * for functions that calculate the PSF.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.18 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-09-25 22:05:05 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_PSF_H
+# define PM_PSF_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+// type of model carried by the pmModel structure
+typedef int pmModelType;
+
+/** pmPSF data structure
+ *
+ * It is useful to generate a model to define the point-spread-function which
+ * describes the flux distribution for unresolved sources in an image. In
+ * general, the PSF varies with position in the image. We allow any of the source
+ * models defined for the pmModel to represent the PSF. For a given source model,
+ * the 2D spatial variation of all of the source parameters, except the first
+ * four PSF-independent parameters, are represented as polynomial, stored in a
+ * psArray. The other elements of the structure define the quality of the PSF
+ * determination.
+ *
+ */
+typedef struct
+{
+    pmModelType type;			///< PSF Model in use
+    psArray *params;			///< Model parameters (psPolynomial2D)
+    psStats *psfTrendStats;		///< psf parameter trend clipping stats
+    pmTrend2DMode psfTrendMode;
+    psPolynomial1D *ChiTrend;           ///< Chisq vs flux fit (correction for systematic errors)
+    pmTrend2D *ApTrend;                 ///< ApResid vs (x,y)
+    pmTrend2D *FluxScale;               ///< Flux for PSF at (x,y) for normalization = 1.0
+    float ApResid;                      ///< apMag - psfMag (for PSF stars)
+    float dApResid;                     ///< scatter of ApResid
+    float skyBias;                      ///< implied residual sky offset from ApResid fit
+    float skySat;                       ///< roll-over of ApResid fit
+    float chisq;                        ///< PSF goodness statistic (unused??)
+    int nPSFstars;                      ///< number of stars used to measure PSF
+    int nApResid;                       ///< number of stars used to measure ApResid
+    int trendNx;
+    int trendNy;
+    int fieldNx;
+    int fieldNy;
+    int fieldXo;
+    int fieldYo;
+    bool poissonErrorsPhotLMM;		///< use poission errors for non-linear model fitting
+    bool poissonErrorsPhotLin;		///< use poission errors for linear model fitting
+    bool poissonErrorsParams;		///< use poission errors for model parameter fitting
+    pmGrowthCurve *growth;              ///< apMag vs Radius
+    pmResiduals *residuals;             ///< normalized residual image (no spatial variation)
+}
+pmPSF;
+
+typedef struct {
+    pmModelType   type;
+    psStats      *stats;		// psfTrend clipping stats
+    pmTrend2DMode psfTrendMode;
+    int           psfTrendNx;
+    int           psfTrendNy;
+    int           psfFieldNx;
+    int           psfFieldNy;
+    int           psfFieldXo;
+    int           psfFieldYo;
+    bool 	  poissonErrorsPhotLMM;	///< use poission errors for non-linear model fitting
+    bool 	  poissonErrorsPhotLin;	///< use poission errors for linear model fitting
+    bool 	  poissonErrorsParams; ///< use poission errors for model parameter fitting
+    float         radius;
+} pmPSFOptions;
+
+# define PM_PAR_E0 PM_PAR_SXX
+# define PM_PAR_E1 PM_PAR_SYY
+# define PM_PAR_E2 PM_PAR_SXY
+
+/**
+ *
+ * Allocator for the pmPSF structure.
+ *
+ */
+pmPSF *pmPSFAlloc (pmPSFOptions *options);
+pmPSFOptions *pmPSFOptionsAlloc ();
+
+double pmPSF_SXYfromModel (psF32 *modelPar);
+double pmPSF_SXYtoModel (psF32 *fittedPar);
+
+bool pmGrowthCurveGenerate (pmReadout *readout, pmPSF *psf, bool ignore, psMaskType maskVal, psMaskType mark);
+pmPSF *pmPSFBuildSimple (char *typeName, float sxx, float syy, float sxy, ...);
+
+bool pmPSF_AxesToModel (psF32 *modelPar, psEllipseAxes axes);
+bool pmPSF_FitToModel (psF32 *fittedPar, float minMinorAxis);
+
+psEllipsePol pmPSF_ModelToFit (psF32 *modelPar);
+psEllipseAxes pmPSF_ModelToAxes (psF32 *modelPar, double maxAR);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/objects/pmPSF_IO.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmPSF_IO.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmPSF_IO.c	(revision 15358)
@@ -0,0 +1,766 @@
+/** @file  pmPSF_IO.c
+ *
+ * This file contains functions to read and write PSF models using the psMetadata Config file
+ * format.
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.27 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-10-09 19:26:25 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+/*****************************************************************************/
+/* INCLUDE FILES                                                             */
+/*****************************************************************************/
+
+#include <string.h>
+#include <strings.h>            /* for strn?casecmp */
+
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmPSF_IO.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourceIO.h"
+
+bool pmPSFmodelCheckDataStatusForView (const pmFPAview *view, const pmFPAfile *file)
+{
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        bool exists = pmPSFmodelCheckDataStatusForFPA (fpa);
+        return exists;
+    }
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        bool exists = pmPSFmodelCheckDataStatusForChip (chip);
+        return exists;
+    }
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
+        return false;
+    }
+    psError(PS_ERR_IO, false, "PSF only valid at the chip level");
+    return false;
+}
+
+bool pmPSFmodelCheckDataStatusForFPA (const pmFPA *fpa) {
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        if (pmPSFmodelCheckDataStatusForChip (chip)) return true;
+    }
+    return false;
+}
+
+bool pmPSFmodelCheckDataStatusForChip (const pmChip *chip) {
+
+    bool status;
+
+    // select the psf of interest
+    pmPSF *psf = psMetadataLookupPtr(&status, chip->analysis, "PSPHOT.PSF");
+    return psf ? true : false;
+}
+
+bool pmPSFmodelWriteForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        if (!pmPSFmodelWriteFPA(fpa, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write PSF for fpa");
+            return false;
+        }
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        if (!pmPSFmodelWriteChip (chip, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write PSF for chip");
+            return false;
+        }
+        return true;
+    }
+
+    psError(PS_ERR_IO, false, "PSF must be written at the chip level");
+    return false;
+}
+
+// read in all chip-level PSFmodel files for this FPA
+bool pmPSFmodelWriteFPA (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        if (!pmPSFmodelWriteChip (chip, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write PSF for %dth chip", i);
+            psFree(thisView);
+            return false;
+        }
+    }
+    psFree(thisView);
+    return true;
+}
+
+// read in all cell-level PSFmodel files for this chip
+bool pmPSFmodelWriteChip (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    if (!pmPSFmodelWrite (chip->analysis, view, file, config)) {
+        psError(PS_ERR_IO, false, "Failed to write PSF for chip");
+        return false;
+    }
+    return true;
+}
+
+// for a pmPSF supplied on the analysis metadata, we write out
+// if needed:
+//   - a PHU blank header
+// - image header        : FITS Image NAXIS = 0
+// if (trendMode == MAP)
+//   - psf resid (+header) : FITS Image
+// else
+//   - psf table (+header) : FITS Table
+bool pmPSFmodelWrite (psMetadata *analysis, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    bool status;
+    pmHDU *hdu;
+    char *headName, *tableName, *residName;
+
+    if (!analysis) return false;
+
+    // select the current recipe
+    psMetadata *recipe = psMetadataLookupPtr (NULL, config->recipes, "PSPHOT");
+    if (!recipe) {
+        psError(PS_ERR_UNKNOWN, false, "missing recipe %s", "PSPHOT");
+        return false;
+    }
+
+    // write a PHU? (only if input image is MEF)
+    // write a header? (only if this is the first readout for cell)
+    //   note that the file->header is set to track the last hdu->header written
+    // write the data? (always?)
+
+    // get the current header
+    hdu = pmFPAviewThisHDU (view, file->fpa);
+
+    // if file does not yet have a PHU, attempt to write one to disk
+    // we only need a PHU if chips->n > 1 and file->fileLevel == FPA
+    // otherwise, the chip header fills the PHU location
+    // XXX this code could be placed in a 'pmPSF_WritePHU' function and called
+    // from pmFPAfileIO.c.
+
+    // define the EXTNAME values used for image header, table data, and residual image segments
+    {
+        // lookup the EXTNAME values used for table data and image header segments
+        char *rule = NULL;
+
+        // Menu of EXTNAME rules
+        psMetadata *menu = psMetadataLookupMetadata(&status, file->camera, "EXTNAME.RULES");
+        if (!menu) {
+            psError(PS_ERR_UNKNOWN, true, "missing EXTNAME.RULES in camera.config");
+            return false;
+        }
+
+        // EXTNAME for image header
+        rule = psMetadataLookupStr(&status, menu, "PSF.HEAD");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, false, "missing entry for PSF.HEAD in EXTNAME.RULES in camera.config");
+            return false;
+        }
+        headName = pmFPAfileNameFromRule (rule, file, view);
+
+        // EXTNAME for table data
+        rule = psMetadataLookupStr(&status, menu, "PSF.TABLE");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, false, "missing entry for PSF.TABLE in EXTNAME.RULES in camera.config");
+            psFree (headName);
+            return false;
+        }
+        tableName = pmFPAfileNameFromRule (rule, file, view);
+
+        // EXTNAME for resid data
+        rule = psMetadataLookupStr(&status, menu, "PSF.RESID");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, false, "missing entry for PSF.RESID in EXTNAME.RULES in camera.config");
+            psFree (headName);
+            psFree (tableName);
+            return false;
+        }
+        residName = pmFPAfileNameFromRule (rule, file, view);
+    }
+
+    // write out the IMAGE header segment
+    // if this header block is new, write it to disk
+    if (hdu->header != file->header) {
+        // add EXTNAME, EXTHEAD, EXTTYPE to header
+        psMetadataAddStr (hdu->header, PS_LIST_TAIL, "EXTTABLE", PS_META_REPLACE, "name of table extension", tableName);
+        psMetadataAddStr (hdu->header, PS_LIST_TAIL, "EXTRESID", PS_META_REPLACE, "name of resid extension", residName);
+        psMetadataAddStr (hdu->header, PS_LIST_TAIL, "EXTTYPE", PS_META_REPLACE, "extension type", "IMAGE");
+        if (!file->wrote_phu) {
+            // this hdu->header acts as the PHU: set EXTEND to be true
+            psMetadataAddBool (hdu->header, PS_LIST_TAIL, "EXTEND", PS_META_REPLACE, "this file has extensions", true);
+            file->wrote_phu = true;
+        }
+
+        psFitsWriteBlank (file->fits, hdu->header, headName);
+        psTrace ("pmFPAfile", 5, "wrote ext head %s (type: %d)\n", file->filename, file->type);
+        file->header = hdu->header;
+        psFree (headName);
+    }
+
+    // select the psf of interest
+    pmPSF *psf = psMetadataLookupPtr (&status, analysis, "PSPHOT.PSF");
+    if (!psf) {
+        psError(PS_ERR_UNKNOWN, true, "missing PSF for this analysis metadata");
+        psFree (tableName);
+        psFree (residName);
+        return false;
+    }
+
+    // write the PSF model parameters in a FITS table
+    {
+        // we need to write a header for the table,
+        psMetadata *header = psMetadataAlloc();
+
+        char *modelName = pmModelClassGetName (psf->type);
+        psMetadataAddStr (header, PS_LIST_TAIL, "PSF_NAME", 0, "PSF model name", modelName);
+
+        psMetadataAddBool (header, PS_LIST_TAIL, "ERR_LMM",  0, "Use Poisson errors in fits?", psf->poissonErrorsPhotLMM);
+        psMetadataAddBool (header, PS_LIST_TAIL, "ERR_LIN",  0, "Use Poisson errors in fits?", psf->poissonErrorsPhotLin);
+        psMetadataAddBool (header, PS_LIST_TAIL, "ERR_PAR",  0, "Use Poisson errors in fits?", psf->poissonErrorsParams);
+
+        int nPar = pmModelClassParameterCount (psf->type)    ;
+        psMetadataAdd (header, PS_LIST_TAIL, "PSF_NPAR", PS_DATA_S32, "PSF model parameter count", nPar);
+
+        psMetadataAddS32 (header, PS_LIST_TAIL, "IMAXIS1", 0, "Image X Size", psf->fieldNx);
+        psMetadataAddS32 (header, PS_LIST_TAIL, "IMAXIS2", 0, "Image Y Size", psf->fieldNy);
+        psMetadataAddS32 (header, PS_LIST_TAIL, "IMREF1",  0, "Image X Ref",  psf->fieldXo);
+        psMetadataAddS32 (header, PS_LIST_TAIL, "IMREF2",  0, "Image Y Ref",  psf->fieldYo);
+
+        // extract PSF Clump info
+        pmPSFClump psfClump;
+
+        psfClump.X  = psMetadataLookupF32 (&status, recipe, "PSF.CLUMP.X");   assert (status);
+        psfClump.Y  = psMetadataLookupF32 (&status, recipe, "PSF.CLUMP.Y");   assert (status);
+        psfClump.dX = psMetadataLookupF32 (&status, recipe, "PSF.CLUMP.DX");  assert (status);
+        psfClump.dY = psMetadataLookupF32 (&status, recipe, "PSF.CLUMP.DY");  assert (status);
+
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PSF_CLX", 0, "psf clump center", psfClump.X);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PSF_CLY", 0, "psf clump center", psfClump.Y);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PSF_CLDX", 0, "psf clump size", psfClump.dX);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PSF_CLDY", 0, "psf clump size", psfClump.dY);
+
+        // save the dimensions of each parameter
+        for (int i = 0; i < nPar; i++) {
+            char name[9];
+            int nX, nY;
+
+            pmTrend2D *trend = psf->params->data[i];
+            if (trend == NULL) continue;
+
+            if (trend->mode == PM_TREND_MAP) {
+              nX = trend->map->map->numCols;
+              nY = trend->map->map->numRows;
+            } else {
+              nX = trend->poly->nX;
+              nY = trend->poly->nY;
+            }
+            snprintf (name, 9, "PAR%02d_NX", i);
+            psMetadataAddS32 (header, PS_LIST_TAIL, name, 0, "", nX);
+            snprintf (name, 9, "PAR%02d_NY", i);
+            psMetadataAddS32 (header, PS_LIST_TAIL, name, 0, "", nY);
+            snprintf (name, 9, "PAR%02d_MD", i);
+            char *modeName = pmTrend2DModeToString (trend->mode);
+            psMetadataAddStr (header, PS_LIST_TAIL, name, 0, "", modeName);
+            psFree (modeName);
+        }
+
+        // other required information describing the PSF
+        psMetadataAddF32 (header, PS_LIST_TAIL, "AP_RESID", 0, "aperture residual", psf->ApResid);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "AP_ERROR", 0, "aperture residual scatter", psf->dApResid);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CHISQ",    0, "chi-square for fit", psf->chisq);
+        psMetadataAddS32 (header, PS_LIST_TAIL, "NSTARS",   0, "number of stars used to measure PSF", psf->nPSFstars);
+
+        // XXX can we drop this now?
+        psMetadataAddF32 (header, PS_LIST_TAIL, "SKY_BIAS", PS_DATA_F32, "sky bias level", psf->skyBias);
+
+        // build a FITS table of the PSF parameters
+        psArray *psfTable = psArrayAllocEmpty (100);
+        for (int i = 0; i < nPar; i++) {
+            pmTrend2D *trend = psf->params->data[i];
+            if (trend == NULL) continue; // skip unset parameters (eg, XPOS)
+
+            if (trend->mode == PM_TREND_MAP) {
+                // write the image components into a table: this is needed because they may each be a different size
+                psImageMap *map = trend->map;
+                for (int ix = 0; ix < map->map->numCols; ix++) {
+                    for (int iy = 0; iy < map->map->numRows; iy++) {
+                        psMetadata *row = psMetadataAlloc ();
+                        psMetadataAddS32 (row, PS_LIST_TAIL, "MODEL_TERM", 0, "", i);
+                        psMetadataAddS32 (row, PS_LIST_TAIL, "X_POWER",    0, "", ix);
+                        psMetadataAddS32 (row, PS_LIST_TAIL, "Y_POWER",    0, "", iy);
+                        psMetadataAddF32 (row, PS_LIST_TAIL, "VALUE",      0, "", map->map->data.F32[iy][ix]);
+                        psMetadataAddF32 (row, PS_LIST_TAIL, "ERROR",      0, "", map->error->data.F32[iy][ix]);
+                        psMetadataAddU8  (row, PS_LIST_TAIL, "MASK",       0, "", 0); // no cells are masked
+
+                        psArrayAdd (psfTable, 100, row);
+                        psFree (row);
+                    }
+                }
+            } else {
+                // write the polynomial components into a table
+                psPolynomial2D *poly = trend->poly;
+                for (int ix = 0; ix <= poly->nX; ix++) {
+                    for (int iy = 0; iy <= poly->nY; iy++) {
+                        psMetadata *row = psMetadataAlloc ();
+                        psMetadataAddS32 (row, PS_LIST_TAIL, "MODEL_TERM", 0, "", i);
+                        psMetadataAddS32 (row, PS_LIST_TAIL, "X_POWER",    0, "", ix);
+                        psMetadataAddS32 (row, PS_LIST_TAIL, "Y_POWER",    0, "", iy);
+                        psMetadataAddF32 (row, PS_LIST_TAIL, "VALUE",      0, "", poly->coeff[ix][iy]);
+                        psMetadataAddF32 (row, PS_LIST_TAIL, "ERROR",      0, "", poly->coeffErr[ix][iy]);
+                        psMetadataAddU8  (row, PS_LIST_TAIL, "MASK",       0, "", poly->coeffMask[ix][iy]);
+
+                        psArrayAdd (psfTable, 100, row);
+                        psFree (row);
+                    }
+                }
+            }
+        }
+
+        // write an empty FITS segment if we have no PSF information
+        if (psfTable->n == 0) {
+            // XXX this is probably an error (if we have a PSF, how do we have no data?)
+            psFitsWriteBlank (file->fits, header, tableName);
+        } else {
+            psTrace ("pmFPAfile", 5, "writing psf data %s\n", tableName);
+            if (!psFitsWriteTable (file->fits, header, psfTable, tableName)) {
+                psError(PS_ERR_IO, false, "writing psf table data %s\n", tableName);
+                psFree (tableName);
+                psFree (residName);
+                psFree (psfTable);
+                psFree (header);
+                return false;
+            }
+        }
+        psFree (tableName);
+        psFree (psfTable);
+        psFree (header);
+    }
+
+    // write the residual images (3D)
+    {
+        psMetadata *header = psMetadataAlloc ();
+        if (psf->residuals == NULL) {
+            // set some header keywords to make it clear there are no residuals?
+            psFitsWriteBlank (file->fits, header, residName);
+            psFree (residName);
+            psFree (header);
+            return true;
+        }
+
+        psMetadataAddS32 (header, PS_LIST_TAIL, "XBIN",    0, "", psf->residuals->xBin);
+        psMetadataAddS32 (header, PS_LIST_TAIL, "YBIN",    0, "", psf->residuals->yBin);
+        psMetadataAddS32 (header, PS_LIST_TAIL, "XCENTER", 0, "", psf->residuals->xCenter);
+        psMetadataAddS32 (header, PS_LIST_TAIL, "YCENTER", 0, "", psf->residuals->yCenter);
+
+        // write the residuals as three planes of the image
+        // this call creates an extension with NAXIS3 = 3
+        if (psf->residuals->Rx) {
+            // this call creates an extension with NAXIS3 = 3
+            psArray *images = psArrayAllocEmpty (3);
+            psArrayAdd (images, 1, psf->residuals->Ro);
+            psArrayAdd (images, 1, psf->residuals->Rx);
+            psArrayAdd (images, 1, psf->residuals->Ry);
+
+            psFitsWriteImageCube (file->fits, header, images, residName);
+            psFree (images);
+        } else {
+            // this call creates an extension with NAXIS3 = 1
+            psFitsWriteImage(file->fits, header, psf->residuals->Ro, 0, residName);
+        }
+        psFree (residName);
+        psFree (header);
+    }
+
+    return true;
+
+    // XXX save the growth curve
+    // XXX save ApTrend (as image?)
+    // XXX write the ApTrend with the same API as will be used for the PSF parameters above
+
+# if (0)
+    // build a FITS table of the fit to the Aperture Residuals
+    psArray *apresTable = psArrayAllocEmpty (100);
+    psPolynomial4D *poly = psf->ApTrend;
+    for (int ix = 0; ix < poly->nX; ix++) {
+        for (int iy = 0; iy < poly->nY; iy++) {
+
+            row = psMetadataAlloc ();
+            psMetadataAddS32 (row, PS_LIST_TAIL, "X_POWER",    0, "", ix);
+            psMetadataAddS32 (row, PS_LIST_TAIL, "Y_POWER",    0, "", iy);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "VALUE",      0, "", poly->coeff[ix][iy]);
+            psMetadataAddF32 (row, PS_LIST_TAIL, "ERROR",      0, "", poly->coeffErr[ix][iy]);
+            psMetadataAddU8  (row, PS_LIST_TAIL, "MASK",       0, "", poly->mask[ix][iy]);
+
+            psArrayAdd (psfTable, 100, row);
+            psFree (row);
+        }
+    }
+# endif
+}
+
+// if this file needs to have a PHU written out, write one
+bool pmPSFmodelWritePHU (const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    // not needed if already written
+    if (file->wrote_phu) return true;
+
+    // not needed if not FPA
+    // XXX this prevents us from defining a SPLIT/MEF CMF file...
+    if (file->fileLevel != PM_FPA_LEVEL_FPA) return true;
+
+    // not needed if only one chip
+    if (file->fpa->chips->n == 1) return true;
+
+    // find the FPA phu
+    pmHDU *phu = pmFPAviewThisPHU (view, file->fpa);
+
+    // if there is no PHU, this is a single header+image (extension-less) file. This could be
+    // the case for an input SPLIT set of files being written out as a MEF.  if there is a PHU,
+    // write it out as a 'blank'
+    psMetadata *outhead = psMetadataAlloc();
+    if (phu) {
+        psMetadataCopy (outhead, phu->header);
+    } else {
+        pmConfigConformHeader (outhead, file->format);
+
+        psMetadata *fileData = psMetadataLookupMetadata(NULL, file->format, "FILE"); // File information
+        const char *fpaNameHdr = psMetadataLookupStr(NULL, fileData, "FPA.NAME");
+        if (fpaNameHdr && strlen(fpaNameHdr) > 0) {
+            const char *fpaName = psMetadataLookupStr(NULL, file->fpa->concepts, "FPA.NAME");
+            psMetadataAddStr(outhead, PS_LIST_TAIL, fpaNameHdr, PS_META_REPLACE, "FPA name", fpaName);
+        }
+    }
+
+    psMetadataAddBool (outhead, PS_LIST_TAIL, "EXTEND", PS_META_REPLACE, "this file has extensions", true);
+    psFitsWriteBlank (file->fits, outhead, "");
+    file->wrote_phu = true;
+
+    psTrace ("pmFPAfile", 5, "wrote phu %s (type: %d)\n", file->filename, file->type);
+    psFree (outhead);
+
+    return true;
+}
+
+bool pmPSFmodelReadForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        return pmPSFmodelReadFPA(fpa, view, file, config);
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        psAbort("Programming error: view does not apply to FPA.");
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        return pmPSFmodelReadChip(chip, view, file, config);
+    }
+
+    psError(PS_ERR_IO, false, "PSF must be read at the chip level");
+    return false;
+}
+
+// read in all chip-level PSFmodel files for this FPA
+bool pmPSFmodelReadFPA (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    bool success = true;                // Was everything successful?
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        success &= pmPSFmodelReadChip(chip, view, file, config);
+    }
+    return success;
+}
+
+// read in all cell-level PSFmodel files for this chip
+bool pmPSFmodelReadChip (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    if (!pmPSFmodelRead (chip->analysis, view, file, config)) {
+        psError(PS_ERR_IO, false, "Failed to write PSF for chip");
+        return false;
+    }
+    return true;
+}
+
+// for each Readout (ie, analysed image), we write out: header + table with PSF model parameters,
+// and header + image for the PSF residual images
+bool pmPSFmodelRead (psMetadata *analysis, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    bool status;
+    char *rule = NULL;
+    psMetadata *header = NULL;
+
+    psTrace ("psModules.objects", 5, "read psf model for %s\n", file->filename);
+
+    // select the current recipe
+    psMetadata *recipe = psMetadataLookupPtr (NULL, config->recipes, "PSPHOT");
+    if (!recipe) {
+        psError(PS_ERR_UNKNOWN, false, "missing recipe %s", "PSPHOT");
+        return false;
+    }
+
+    // Menu of EXTNAME rules
+    psMetadata *menu = psMetadataLookupMetadata(&status, file->camera, "EXTNAME.RULES");
+    if (!menu) {
+        psError(PS_ERR_UNKNOWN, true, "missing EXTNAME.RULES in camera.config");
+        return false;
+    }
+    // EXTNAME for table data
+    rule = psMetadataLookupStr(&status, menu, "PSF.TABLE");
+    if (!rule) {
+        psError(PS_ERR_UNKNOWN, true, "missing entry for PSF.TABLE in EXTNAME.RULES in camera.config");
+        return false;
+    }
+    char *tableName = pmFPAfileNameFromRule (rule, file, view);
+    // EXTNAME for residual images
+    rule = psMetadataLookupStr(&status, menu, "PSF.RESID");
+    if (!rule) {
+        psError(PS_ERR_UNKNOWN, true, "missing entry for PSF.RESID in EXTNAME.RULES in camera.config");
+        return false;
+    }
+    char *imageName = pmFPAfileNameFromRule (rule, file, view);
+
+    // move fits pointer to table and read header
+    // advance to the table data extension
+    // since we have read the IMAGE header, the TABLE header should exist
+    if (!psFitsMoveExtName (file->fits, tableName)) {
+        psAbort("cannot find data extension %s in %s", tableName, file->filename);
+    }
+
+    // load the PSF model table header
+    header = psFitsReadHeader (NULL, file->fits);
+    if (!header) psAbort("cannot read table header");
+
+    pmPSFOptions *options = pmPSFOptionsAlloc();
+
+    // load the PSF model parameters from the FITS table
+    char *modelName = psMetadataLookupStr (&status, header, "PSF_NAME");
+    options->type = pmModelClassGetType (modelName);
+    if (options->type == -1) {
+        psError(PS_ERR_UNKNOWN, true, "invalid model name %s in psf file %s", modelName, file->filename);
+        return false;
+    }
+
+    // psf clump data
+    pmPSFClump psfClump;
+
+    psfClump.X  = psMetadataLookupF32 (&status, header, "PSF_CLX" );  assert(status);
+    psfClump.Y  = psMetadataLookupF32 (&status, header, "PSF_CLY" );  assert(status);
+    psfClump.dX = psMetadataLookupF32 (&status, header, "PSF_CLDX");  assert(status);
+    psfClump.dY = psMetadataLookupF32 (&status, header, "PSF_CLDY");  assert(status);
+
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "PSF.CLUMP.X" , PS_META_REPLACE, "psf clump center", psfClump.X);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "PSF.CLUMP.Y" , PS_META_REPLACE, "psf clump center", psfClump.Y);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "PSF.CLUMP.DX", PS_META_REPLACE, "psf clump size",   psfClump.dX);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "PSF.CLUMP.DY", PS_META_REPLACE, "psf clump size",   psfClump.dY);
+
+    options->poissonErrorsPhotLMM = psMetadataLookupBool (&status, header, "ERR_LMM");
+    options->poissonErrorsPhotLin = psMetadataLookupBool (&status, header, "ERR_LIN");
+    options->poissonErrorsParams  = psMetadataLookupBool (&status, header, "ERR_PAR");
+
+    options->psfFieldNx = psMetadataLookupS32 (&status, header, "IMAXIS1");
+    options->psfFieldNy = psMetadataLookupS32 (&status, header, "IMAXIS2");
+    options->psfFieldXo = psMetadataLookupS32 (&status, header, "IMREF1");
+    options->psfFieldYo = psMetadataLookupS32 (&status, header, "IMREF2");
+
+    psImageBinning *binning = psImageBinningAlloc();
+    binning->nXfine = options->psfFieldNx;
+    binning->nYfine = options->psfFieldNy;
+
+    // we determine the PSF parameter polynomials from the MD-defined polynomials
+    pmPSF *psf = pmPSFAlloc (options);
+
+    // check the number of expected parameters
+    int nPar = psMetadataLookupS32 (&status, header, "PSF_NPAR");
+    if (nPar != pmModelClassParameterCount (psf->type))
+        psAbort("mismatch model par count");
+
+    // load the trend mode and dimensions of each parameter
+    for (int i = 0; i < nPar; i++) {
+        char name[9];
+        snprintf (name, 9, "PAR%02d_NX", i);
+        binning->nXruff = psMetadataLookupS32 (&status, header, name);
+        if (!status) continue;          // not all parameters are defined
+
+        snprintf (name, 9, "PAR%02d_NY", i);
+        binning->nYruff = psMetadataLookupS32 (&status, header, name);
+        if (!status) {
+            psError(PS_ERR_UNKNOWN, true, "inconsistent PSF header: NX defined for PAR %d, but not NY", i);
+            return false;
+        }
+
+        snprintf (name, 9, "PAR%02d_MD", i);
+        char *modeName = psMetadataLookupStr (&status, header, name);
+        if (!status) {
+            psError(PS_ERR_UNKNOWN, true, "inconsistent PSF header: NX & NY defined for PAR %d, but not MD", i);
+            return false;
+        }
+        pmTrend2DMode psfTrendMode = pmTrend2DModeFromString (modeName);
+        if (psfTrendMode == PM_TREND_NONE) {
+            psfTrendMode = PM_TREND_POLY_ORD;
+        }
+
+        // XXX Attempting to guard against failing assertions on nXruff and nYruff in psImageBinningSetScale.
+        // This replicates code in psphotCheckStarDistribution, where these values are generated.  Not sure
+        // it's correct, though.
+        if (psfTrendMode != PM_TREND_MAP) {
+            binning->nXruff++;
+            binning->nYruff++;
+        }
+
+        psImageBinningSetScale (binning, PS_IMAGE_BINNING_CENTER);
+        psImageBinningSetSkipByOffset (binning, options->psfFieldXo, options->psfFieldYo);
+        psf->params->data[i] = pmTrend2DNoImageAlloc (psfTrendMode, binning, NULL);
+    }
+    psFree (binning);
+
+    // other required information describing the PSF
+    psf->ApResid   = psMetadataLookupF32 (&status, header, "AP_RESID");
+    psf->dApResid  = psMetadataLookupF32 (&status, header, "AP_ERROR");
+    psf->chisq     = psMetadataLookupF32 (&status, header, "CHISQ");
+    psf->nPSFstars = psMetadataLookupS32 (&status, header, "NSTARS");
+
+    // XXX can we drop this now?
+    psf->skyBias   = psMetadataLookupF32 (&status, header, "SKY_BIAS");
+
+    // read the raw table data
+    psArray *table = psFitsReadTable (file->fits);
+
+    // fill in the matching psf->params entries
+    for (int i = 0; i < table->n; i++) {
+        psMetadata *row = table->data[i];
+        int iPar = psMetadataLookupS32 (&status, row, "MODEL_TERM");
+        int xPow = psMetadataLookupS32 (&status, row, "X_POWER");
+        int yPow = psMetadataLookupS32 (&status, row, "Y_POWER");
+
+        pmTrend2D *trend = psf->params->data[iPar];
+        if (trend == NULL) {
+            psError(PS_ERR_UNKNOWN, true, "parameter %d not available", iPar);
+            return false;
+        }
+
+        if (trend->mode == PM_TREND_MAP) {
+            psImageMap *map = trend->map;
+            assert (map);
+            assert (map->map);
+            assert (map->error);
+            assert (xPow >= 0);
+            assert (yPow >= 0);
+            assert (xPow < map->map->numCols);
+            assert (yPow < map->map->numRows);
+            map->map->data.F32[yPow][xPow]    = psMetadataLookupF32 (&status, row, "VALUE");
+            map->error->data.F32[yPow][xPow]  = psMetadataLookupF32 (&status, row, "ERROR");
+        } else {
+            psPolynomial2D *poly = trend->poly;
+            assert (poly);
+            assert (xPow >= 0);
+            assert (yPow >= 0);
+            assert (xPow <= poly->nX);
+            assert (yPow <= poly->nY);
+            poly->coeff[xPow][yPow]     = psMetadataLookupF32 (&status, row, "VALUE");
+            poly->coeffErr[xPow][yPow]  = psMetadataLookupF32 (&status, row, "ERROR");
+            poly->coeffMask[xPow][yPow] = psMetadataLookupU8  (&status, row, "MASK");
+        }
+    }
+    psFree (header);
+    psFree (table);
+
+    // move fits pointer to residual image and read header
+    // advance to the table data extension
+    // since we have read the IMAGE header, the TABLE header should exist
+    if (!psFitsMoveExtName (file->fits, imageName)) {
+        psAbort("cannot find data extension %s in %s", imageName, file->filename);
+    }
+
+    header = psFitsReadHeader (NULL, file->fits);
+    int Naxis = psMetadataLookupS32 (&status, header, "NAXIS");
+    if (Naxis != 0) {
+
+        int Nx = psMetadataLookupS32 (&status, header, "NAXIS1");
+        int Ny = psMetadataLookupS32 (&status, header, "NAXIS2");
+        int Nz = psMetadataLookupS32 (&status, header, "NAXIS3");
+
+        int xBin  = psMetadataLookupS32 (&status, header, "XBIN");
+        int yBin  = psMetadataLookupS32 (&status, header, "YBIN");
+
+        int xSize = Nx / xBin;
+        int ySize = Ny / yBin;
+
+        psf->residuals = pmResidualsAlloc (xSize, ySize, xBin, yBin);
+
+        psf->residuals->xCenter = psMetadataLookupS32 (&status, header, "XCENTER");
+        psf->residuals->yCenter = psMetadataLookupS32 (&status, header, "YCENTER");
+
+        psRegion fullImage = {0, 0, 0, 0};
+        psFitsReadImageBuffer(psf->residuals->Ro, file->fits, fullImage, 0); // Desired pixels
+        if (Nz > 1) {
+            assert (Nz == 3);
+            psFitsReadImageBuffer(psf->residuals->Rx, file->fits, fullImage, 1); // Desired pixels
+            psFitsReadImageBuffer(psf->residuals->Ry, file->fits, fullImage, 2); // Desired pixels
+        }
+    }
+
+    psMetadataAdd (analysis, PS_LIST_TAIL, "PSPHOT.PSF",     PS_DATA_UNKNOWN,  "psphot psf", psf);
+    psFree (psf);
+
+    psFree (tableName);
+    psFree (imageName);
+    psFree (header);
+
+    return true;
+}
+
+// create a psMetadata representation (human-readable) of a psf model
+// XXX pmPSF to/from Metadata functions were defined for 1.22 and earlier, but were dropped
Index: /branches/eam_branch_20071023/psModules/src/objects/pmPSF_IO.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmPSF_IO.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmPSF_IO.h	(revision 15358)
@@ -0,0 +1,36 @@
+/* @file  pmPSF.h
+ *
+ * This file contains typedefs for the Point-Spread Function and prototypes
+ * for functions that calculate the PSF.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.10 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-09-24 21:27:58 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_PSF_IO_H
+# define PM_PSF_IO_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+bool pmPSFmodelWriteForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmPSFmodelWriteFPA (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmPSFmodelWriteChip (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmPSFmodelWrite (psMetadata *analysis, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+
+bool pmPSFmodelWritePHU (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+
+bool pmPSFmodelReadForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmPSFmodelReadFPA (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmPSFmodelReadChip (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmPSFmodelRead (psMetadata *analysis, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+
+bool pmPSFmodelCheckDataStatusForView (const pmFPAview *view, const pmFPAfile *file);
+bool pmPSFmodelCheckDataStatusForFPA (const pmFPA *fpa);
+bool pmPSFmodelCheckDataStatusForChip (const pmChip *chip);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/objects/pmPSFtoMetadata.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmPSFtoMetadata.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmPSFtoMetadata.c	(revision 15358)
@@ -0,0 +1,91 @@
+
+// create a psMetadata representation (human-readable) of a psf model
+psMetadata *pmPSFtoMetadata (psMetadata *metadata, pmPSF *psf)
+{
+
+    if (metadata == NULL) {
+        metadata = psMetadataAlloc ();
+    }
+
+    char *modelName = pmModelClassGetName (psf->type);
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_MODEL_NAME", PS_DATA_STRING, "PSF model name", modelName);
+
+    int nPar = pmModelClassParameterCount (psf->type)    ;
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_MODEL_NPAR", PS_DATA_S32, "PSF model parameter count", nPar);
+
+    for (int i = 0; i < nPar; i++) {
+        psPolynomial2D *poly = psf->params->data[i];
+        if (poly == NULL)
+            continue;
+        psPolynomial2DtoMetadata (metadata, poly, "PSF_PAR%02d", i);
+    }
+
+    // XXX fix this
+    psWarning ("APTREND is currently missing");
+    // psPolynomial4DtoMetadata (metadata, psf->ApTrend, "APTREND");
+
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_AP_RESID", PS_DATA_F32, "aperture residual", psf->ApResid);
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_dAP_RESID", PS_DATA_F32, "aperture residual scatter", psf->dApResid);
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_SKY_BIAS", PS_DATA_F32, "sky bias level", psf->skyBias);
+
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_CHISQ", PS_DATA_F32, "chi-square for fit", psf->chisq);
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_NSTARS", PS_DATA_S32, "number of stars used to measure PSF", psf->nPSFstars);
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_POISSON_ERRORS", PS_DATA_BOOL, "Poisson errors for fits", psf->poissonErrors);
+
+    return metadata;
+}
+
+// parse a psMetadata representation (human-readable) of a psf model
+pmPSF *pmPSFfromMetadata (psMetadata *metadata)
+{
+
+    bool status;
+    char keyword[80];
+
+    char *modelName = psMetadataLookupPtr (&status, metadata, "PSF_MODEL_NAME");
+    pmModelType type = pmModelClassGetType (modelName);
+
+    bool poissonErrors = psMetadataLookupPtr (&status, metadata, "PSF_POISSON_ERRORS");
+    if (!status)
+        poissonErrors = true;
+
+    // we determine the PSF parameter polynomials from the MD-defined polynomials
+    pmPSF *psf = pmPSFAlloc (type, poissonErrors, NULL);
+
+    int nPar = psMetadataLookupS32 (&status, metadata, "PSF_MODEL_NPAR");
+    if (nPar != pmModelClassParameterCount (psf->type))
+        psAbort("mismatch model par count");
+
+    // un-fitted terms, not in the Metadata, are left NULL
+    // XXX add a double-check of the expected number?
+    for (int i = 0; i < nPar; i++) {
+        sprintf (keyword, "PSF_PAR%02d", i);
+        psMetadata *folder = psMetadataLookupPtr (&status, metadata, keyword);
+        if (!status)
+            continue;
+        psPolynomial2D *poly = psPolynomial2DfromMetadata (folder);
+        psFree (psf->params->data[i]);
+        psf->params->data[i] = poly;
+    }
+
+    // load the APTREND data
+    // XXX fix this to work with pmTrend2D
+    psWarning ("APTREND is not being read");
+    # if (0)
+    sprintf (keyword, "APTREND");
+    psMetadata *folder = psMetadataLookupPtr (&status, metadata, keyword);
+    psPolynomial4D *poly = psPolynomial4DfromMetadata (folder);
+    psFree (psf->ApTrend);
+    psf->ApTrend = poly;
+    # endif
+
+    psf->ApResid = psMetadataLookupF32 (&status, metadata, "PSF_AP_RESID");
+    psf->dApResid = psMetadataLookupF32 (&status, metadata, "PSF_dAP_RESID");
+    psf->skyBias = psMetadataLookupF32 (&status, metadata, "PSF_SKY_BIAS");
+
+    psf->chisq = psMetadataLookupF32 (&status, metadata, "PSF_CHISQ");
+    psf->nPSFstars = psMetadataLookupS32 (&status, metadata, "PSF_NSTARS");
+
+    psFree (metadata);
+    return (psf);
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmPSFtry.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmPSFtry.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmPSFtry.c	(revision 15358)
@@ -0,0 +1,793 @@
+/** @file  pmPSFtry.c
+ *
+ *  XXX: need description of file purpose
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.49 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-10-09 19:26:25 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+# include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAMaskWeight.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmSourceUtils.h"
+#include "pmPSFtry.h"
+#include "pmModelClass.h"
+#include "pmModelUtils.h"
+#include "pmSourceFitModel.h"
+#include "pmSourcePhotometry.h"
+
+// ********  pmPSFtry functions  **************************************************
+// * pmPSFtry holds a single pmPSF model test, with the input sources, the freely
+// * fitted version of the model, the pmPSF fit to the fitted model parameters,
+// * and the PSF fits to the source. It also includes the statistics from the
+// * fits, both the individual sources, and the collection
+
+// free a pmPSFtry structure
+static void pmPSFtryFree (pmPSFtry *test)
+{
+    if (test == NULL) return;
+
+    psFree (test->psf);
+    psFree (test->sources);
+    psFree (test->metric);
+    psFree (test->metricErr);
+    psFree (test->fitMag);
+    psFree (test->mask);
+    return;
+}
+
+// allocate a pmPSFtry based on the desired sources and the model (identified by name)
+pmPSFtry *pmPSFtryAlloc (psArray *sources, pmPSFOptions *options)
+{
+    pmPSFtry *test = (pmPSFtry *) psAlloc(sizeof(pmPSFtry));
+    psMemSetDeallocator(test, (psFreeFunc) pmPSFtryFree);
+
+    test->psf       = pmPSFAlloc (options);
+    test->metric    = psVectorAlloc (sources->n, PS_TYPE_F32);
+    test->metricErr = psVectorAlloc (sources->n, PS_TYPE_F32);
+    test->fitMag    = psVectorAlloc (sources->n, PS_TYPE_F32);
+    test->mask      = psVectorAlloc (sources->n, PS_TYPE_U8);
+
+    psVectorInit (test->mask,        0);
+    psVectorInit (test->metric,    0.0);
+    psVectorInit (test->metricErr, 0.0);
+    psVectorInit (test->fitMag,    0.0);
+
+    test->sources   = psArrayAlloc (sources->n);
+
+    for (int i = 0; i < sources->n; i++) {
+        test->sources->data[i] = pmSourceCopy (sources->data[i]);
+    }
+
+    return (test);
+}
+
+// build a pmPSFtry for the given model:
+// - fit each source with the free-floating model
+// - construct the pmPSF from the collection of models
+// - fit each source with the PSF-parameter models
+// - measure the pmPSF quality metric (dApResid)
+
+// sources used in for pmPSFtry may be masked by the analysis
+// mask values indicate the reason the source was rejected:
+
+// generate a pmPSFtry with a copy of the test PSF sources
+pmPSFtry *pmPSFtryModel (psArray *sources, char *modelName, pmPSFOptions *options, psMaskType maskVal, psMaskType mark)
+{
+    bool status;
+    int Next = 0;
+    int Npsf = 0;
+
+    // validate the requested model name
+    options->type = pmModelClassGetType (modelName);
+    if (options->type == -1) {
+        psError (PS_ERR_UNKNOWN, true, "invalid model name %s", modelName);
+        return NULL;
+    }
+
+    pmPSFtry *psfTry = pmPSFtryAlloc (sources, options);
+    if (psfTry == NULL) {
+        psError (PS_ERR_UNKNOWN, false, "failed to allocate psf model");
+        return NULL;
+    }
+
+    // stage 1:  fit an EXT model to all candidates PSF sources
+    psTimerStart ("fit");
+    for (int i = 0; i < psfTry->sources->n; i++) {
+
+        pmSource *source = psfTry->sources->data[i];
+        source->modelEXT = pmSourceModelGuess (source, psfTry->psf->type);
+        if (source->modelEXT == NULL) {
+            psError(PS_ERR_UNKNOWN, false, "failed to build model");
+            psFree(psfTry);
+            return NULL;
+        }
+
+        // set object mask to define valid pixels -- XXX not unset?
+        psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, options->radius, "OR", mark);
+
+        // fit model as EXT, not PSF
+        status = pmSourceFitModel (source, source->modelEXT, PM_SOURCE_FIT_EXT, maskVal);
+
+        // exclude the poor fits
+        if (!status) {
+            psfTry->mask->data.U8[i] = PSFTRY_MASK_EXT_FAIL;
+            continue;
+        }
+        Next ++;
+    }
+    psLogMsg ("psphot.psftry", 4, "fit ext:   %f sec for %d of %ld sources\n", psTimerMark ("fit"), Next, sources->n);
+    psTrace ("psphot.psftry", 3, "keeping %d of %ld PSF candidates (EXT)\n", Next, sources->n);
+
+    // stage 2: construct a psf (pmPSF) from this collection of model fits
+    if (!pmPSFFromPSFtry (psfTry)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to construct a psf model from collection of sources");
+        psFree(psfTry);
+        return NULL;
+    }
+
+    // stage 3: refit with fixed shape parameters
+    psTimerStart ("fit");
+    for (int i = 0; i < psfTry->sources->n; i++) {
+        // masked for: bad model fit, outlier in parameters
+        if (psfTry->mask->data.U8[i] & PSFTRY_MASK_ALL)
+            continue;
+
+        pmSource *source = psfTry->sources->data[i];
+
+        // set shape for this model based on PSF
+        source->modelPSF = pmModelFromPSF (source->modelEXT, psfTry->psf);
+        if (source->modelPSF == NULL) {
+            psfTry->mask->data.U8[i] = PSFTRY_MASK_BAD_MODEL;
+            abort();
+            continue;
+        }
+        source->modelPSF->radiusFit = options->radius;
+
+        // set object mask to define valid pixels -- XXX not unset?
+        psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, options->radius, "OR", mark);
+
+        // fit the PSF model to the source
+        status = pmSourceFitModel (source, source->modelPSF, PM_SOURCE_FIT_PSF, maskVal);
+
+        // skip poor fits
+        if (!status) {
+            psfTry->mask->data.U8[i] = PSFTRY_MASK_PSF_FAIL;
+            psTrace ("psModules.objects", 4, "dropping %d (%d,%d) : failed PSF fit\n", i, source->peak->x, source->peak->y);
+            continue;
+        }
+
+        // XXX : use a different aperture radius from the fit radius?
+        if (!pmSourceMagnitudes (source, psfTry->psf, PM_SOURCE_PHOT_INTERP, maskVal, mark)) {
+            psfTry->mask->data.U8[i] = PSFTRY_MASK_BAD_PHOT;
+            psTrace ("psModules.objects", 4, "dropping %d (%d,%d) : poor photometry\n", i, source->peak->x, source->peak->y);
+            continue;
+        }
+
+        psfTry->fitMag->data.F32[i] = source->psfMag;
+        psfTry->metric->data.F32[i] = source->apMag - source->psfMag;
+        psfTry->metricErr->data.F32[i] = source->errMag;
+        Npsf ++;
+    }
+    psfTry->psf->nPSFstars = Npsf;
+
+    psLogMsg ("psphot.psftry", 4, "fit psf:   %f sec for %d of %ld sources\n", psTimerMark ("fit"), Npsf, sources->n);
+    psTrace ("psphot.psftry", 3, "keeping %d of %ld PSF candidates (PSF)\n", Npsf, sources->n);
+
+    // measure the chi-square trend as a function of flux (PAR[PM_PAR_I0])
+    // this should be linear for Poisson errors and quadratic for constant sky errors
+    psVector *flux  = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);
+    psVector *chisq = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);
+    psVector *mask  = psVectorAlloc (psfTry->sources->n, PS_TYPE_MASK);
+
+    // generate the x and y vectors, and mask missing models
+    for (int i = 0; i < psfTry->sources->n; i++) {
+        pmSource *source = psfTry->sources->data[i];
+        if (source->modelPSF == NULL) {
+            flux->data.F32[i] = 0.0;
+            chisq->data.F32[i] = 0.0;
+            mask->data.U8[i] = 0xff;
+        } else {
+            flux->data.F32[i] = source->modelPSF->params->data.F32[PM_PAR_I0];
+            chisq->data.F32[i] = source->modelPSF->chisq / source->modelPSF->nDOF;
+            mask->data.U8[i] = 0;
+        }
+    }
+
+    // use 3hi/3lo sigma clipping on the chisq fit
+    psStats *stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    stats->clipSigma = 3.0;
+    stats->clipIter = 3;
+
+    // linear clipped fit of chisq trend vs flux
+    bool result = psVectorClipFitPolynomial1D(psfTry->psf->ChiTrend, stats, mask, 0xff, chisq, NULL, flux);
+    psLogMsg ("pmPSFtry", 4, "chisq vs flux fit: %f +/- %f\n", stats->robustMedian, stats->robustStdev);
+
+    psFree (flux);
+    psFree (stats);
+    psFree (mask);
+    psFree (chisq);
+
+    if (!result) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to fit psf->ChiTrend");
+        psFree(psfTry);
+        return NULL;
+    }
+
+    for (int i = 0; i < psfTry->psf->ChiTrend->nX + 1; i++) {
+        psLogMsg ("pmPSFtry", 4, "chisq vs flux fit term %d: %f +/- %f\n", i,
+                  psfTry->psf->ChiTrend->coeff[i]*pow(10000, i),
+                  psfTry->psf->ChiTrend->coeffErr[i]*pow(10000,i));
+    }
+
+    // XXX this function wants aperture radius for pmSourcePhotometry
+    pmPSFtryMetric (psfTry, options->radius);
+    psLogMsg ("psphot.pspsf", 3, "try model %s, ap-fit: %f +/- %f : sky bias: %f\n",
+              modelName, psfTry->psf->ApResid, psfTry->psf->dApResid, psfTry->psf->skyBias);
+
+    return (psfTry);
+}
+
+bool pmPSFtryMetric (pmPSFtry *psfTry, float RADIUS)
+{
+    // the measured (aperture - fit) magnitudes (dA == psfTry->metric)
+    //   depend on both the true ap-fit (dAo) and the bias in the sky measurement:
+    //     dA = dAo + dsky/flux
+    //   where flux is the flux of the star
+    // we fit this trend to find the infinite flux aperture correction (dAo),
+    //   the nominal sky bias (dsky), and the error on dAo
+    // the values of dA are contaminated by stars with close neighbors in the aperture
+    //   we use an outlier rejection to avoid this bias
+
+    // r2rflux = radius^2 * ten(0.4*fitMag);
+    psVector *r2rflux = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);
+
+    for (int i = 0; i < psfTry->sources->n; i++) {
+        if (psfTry->mask->data.U8[i] & PSFTRY_MASK_ALL)
+            continue;
+        r2rflux->data.F32[i] = PS_SQR(RADIUS) * pow(10.0, 0.4*psfTry->fitMag->data.F32[i]);
+    }
+
+    // This analysis of the apResid statistics is only approximate.  The fitted magnitudes
+    // measured at this point (in the PSF fit) use Poisson errors, and are thus biased as a
+    // function of magnitude.  We re-measure the apResid statistics later in psphot using the
+    // linear, constant-error fitting.  Do not reject outliers with excessive vigor here.
+
+    // use 3hi/1lo sigma clipping on the r2rflux vs metric fit
+    psStats *stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    stats->clipSigma = 3.0;
+    stats->clipIter = 3;
+
+    // fit ApTrend only to r2rflux, ignore x,y,flux variations for now
+    // linear clipped fit of ApResid to r2rflux
+    psPolynomial1D *poly = psPolynomial1DAlloc (PS_POLYNOMIAL_ORD, 1);
+    poly->coeffMask[1] = PS_POLY_MASK_SET; // fit only a constant offset (no SKYBIAS)
+
+    // XXX replace this with a psVectorStats call?  since we are not fitting the trend
+    bool result = psVectorClipFitPolynomial1D (poly, stats, psfTry->mask, PSFTRY_MASK_ALL, psfTry->metric, psfTry->metricErr, r2rflux);
+    if (!result) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to fit clipped poly");
+
+        psFree(poly);
+        psFree(r2rflux);
+        psFree(stats);
+
+        return false;
+    }
+    psLogMsg ("pmPSFtryMetric", 4, "apresid: %f +/- %f; keeping %ld of %ld psf stars\n", poly->coeff[0], stats->robustStdev, stats->clippedNvalues, psfTry->sources->n);
+
+    // XXX test dump of fitted model (dump when tracing?)
+    if (psTraceGetLevel("psModules.objects") >= 4) {
+        FILE *f = fopen ("resid.dat", "w");
+        psVector *apfit = psPolynomial1DEvalVector (poly, r2rflux);
+        for (int i = 0; i < psfTry->sources->n; i++) {
+            int keep = (psfTry->mask->data.U8[i] & PSFTRY_MASK_ALL);
+
+            pmSource *source = psfTry->sources->data[i];
+            float x = source->peak->x;
+            float y = source->peak->y;
+
+            fprintf (f, "%d  %d, %f %f %f  %f %f %f  %f\n",
+                     i, keep, x, y,
+                     psfTry->fitMag->data.F32[i],
+                     r2rflux->data.F32[i],
+                     psfTry->metric->data.F32[i],
+                     psfTry->metricErr->data.F32[i],
+                     apfit->data.F32[i]);
+        }
+        fclose (f);
+        psFree (apfit);
+    }
+
+    // XXX drop the skyBias value, or include above??
+    psfTry->psf->skyBias  = poly->coeff[1];
+    psfTry->psf->ApResid  = poly->coeff[0];
+    psfTry->psf->dApResid = stats->robustStdev;
+
+    psFree (r2rflux);
+    psFree (poly);
+    psFree (stats);
+
+    return true;
+}
+
+/*
+  (aprMag' - fitMag) = rflux*skyBias + ApTrend(x,y)
+  (aprMag - rflux*skyBias) - fitMag = ApTrend(x,y)
+  (aprMag - rflux*skyBias) = fitMag + ApTrend(x,y)
+*/
+
+/*****************************************************************************
+pmPSFFromPSFtry (psfTry): build a PSF model from a collection of
+source->modelEXT entries.  The PSF ignores the first 4 (independent) model
+parameters and constructs a polynomial fit to the remaining as a function of
+image coordinate.
+    input: psfTry with fitted source->modelEXT collection, pre-allocated psf
+Note: some of the array entries may be NULL (failed fits); ignore them.
+ *****************************************************************************/
+bool pmPSFFromPSFtry (pmPSFtry *psfTry)
+{
+    pmPSF *psf = psfTry->psf;
+
+    // construct the fit vectors from the collection of objects
+    psVector *x  = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);
+    psVector *y  = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);
+    psVector *z  = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);
+
+    // construct the x,y terms
+    for (int i = 0; i < psfTry->sources->n; i++) {
+        pmSource *source = psfTry->sources->data[i];
+        if (source->modelEXT == NULL)
+            continue;
+
+        x->data.F32[i] = source->modelEXT->params->data.F32[PM_PAR_XPOS];
+        y->data.F32[i] = source->modelEXT->params->data.F32[PM_PAR_YPOS];
+    }
+
+    if (!pmPSFFitShapeParams (psf, psfTry->sources, x, y, psfTry->mask)) {
+	psFree(x);
+	psFree(y);
+	psFree(z);
+	return false;
+    }
+
+    // skip the unfitted parameters (X, Y, Io, Sky) and the shape parameters (SXX, SYY, SXY)
+    // apply the values of Nx, Ny determined above for E0,E1,E2 to the remaining parameters
+    for (int i = 0; i < psf->params->n; i++) {
+        switch (i) {
+          case PM_PAR_SKY:
+          case PM_PAR_I0:
+          case PM_PAR_XPOS:
+          case PM_PAR_YPOS:
+          case PM_PAR_SXX:
+          case PM_PAR_SYY:
+          case PM_PAR_SXY:
+            continue;
+          default:
+            break;
+        }
+
+        // select the per-object fitted data for this parameter
+        for (int j = 0; j < psfTry->sources->n; j++) {
+            pmSource *source = psfTry->sources->data[j];
+            if (source->modelEXT == NULL) continue;
+            z->data.F32[j] = source->modelEXT->params->data.F32[i];
+        }
+
+	psImageBinning *binning = psImageBinningAlloc();
+	binning->nXruff = psf->trendNx;
+	binning->nYruff = psf->trendNy;
+	binning->nXfine = psf->fieldNx;
+	binning->nYfine = psf->fieldNy;
+
+	if (psf->psfTrendMode == PM_TREND_MAP) {
+	    psImageBinningSetScale (binning, PS_IMAGE_BINNING_CENTER);
+	    psImageBinningSetSkipByOffset (binning, psf->fieldXo, psf->fieldYo);
+	}
+
+	// free existing trend, re-alloc
+	psFree (psf->params->data[i]);
+	psf->params->data[i] = pmTrend2DNoImageAlloc (psf->psfTrendMode, binning, psf->psfTrendStats);
+	psFree (binning);
+
+        // fit the collection of measured parameters to the PSF 2D model
+        // the mask is carried from previous steps and updated with this operation
+        // the weight is either the flux error or NULL, depending on 'psf->poissonErrorParams'
+        if (!pmTrend2DFit (psf->params->data[i], psfTry->mask, 0xff, x, y, z, NULL)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to build psf model for parameter %d", i);
+            psFree(x);
+            psFree(y);
+            psFree(z);
+            return false;
+        }
+    }
+
+    // test dump of star parameters vs position (compare with fitted values)
+    if (psTraceGetLevel("psModules.objects") >= 4) {
+        FILE *f = fopen ("params.dat", "w");
+
+        for (int j = 0; j < psfTry->sources->n; j++) {
+            pmSource *source = psfTry->sources->data[j];
+            if (source == NULL) continue;
+            if (source->modelEXT == NULL) continue;
+
+            pmModel *modelPSF = pmModelFromPSF (source->modelEXT, psf);
+
+            fprintf (f, "%f %f : ", source->modelEXT->params->data.F32[PM_PAR_XPOS], source->modelEXT->params->data.F32[PM_PAR_YPOS]);
+
+            for (int i = 0; i < psf->params->n; i++) {
+                if (psf->params->data[i] == NULL) continue;
+                fprintf (f, "%f %f : ", source->modelEXT->params->data.F32[i], modelPSF->params->data.F32[i]);
+            }
+            fprintf (f, "%f %d\n", source->modelEXT->chisq, source->modelEXT->nIter);
+
+            psFree(modelPSF);
+        }
+        fclose (f);
+    }
+
+    psFree (x);
+    psFree (y);
+    psFree (z);
+    return true;
+}
+
+
+bool pmPSFFitShapeParams (pmPSF *psf, psArray *sources, psVector *x, psVector *y, psVector *srcMask) {
+
+    // we are doing a robust fit.  after each pass, we drop points which are more deviant than
+    // three sigma.  mask is currently updated for each pass.
+
+    // The shape parameters (SXX, SXY, SYY) are strongly coupled.  We have to handle them very
+    // carefully.  First, we convert them to the Ellipse Polarization terms (E0, E1, E2) for
+    // each source and fit this set of parameters.  These values are less tightly coupled, but
+    // are still inter-related.  The fitted values do a good job of constraining the major axis
+    // and the position angle, but the minor axis is weakly measured.  When we apply the PSF
+    // model to construct a source model, we convert the fitted values of E0,E1,E2 to the shape
+    // parameters, with the constraint that the minor axis must be greater than a minimum
+    // threshold.
+
+    // convert the measured source shape paramters to polarization terms
+    psVector *e0   = psVectorAlloc (sources->n, PS_TYPE_F32);
+    psVector *e1   = psVectorAlloc (sources->n, PS_TYPE_F32);
+    psVector *e2   = psVectorAlloc (sources->n, PS_TYPE_F32);
+    psVector *mag  = psVectorAlloc (sources->n, PS_TYPE_F32);
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+        if (source->modelEXT == NULL) continue;
+	// XXX I am relying on the fact that none of the masked sources
+	// have modelEXT set here.  perhaps use the value of psfTry->mask instead?
+
+        psEllipsePol pol = pmPSF_ModelToFit (source->modelEXT->params->data.F32);
+
+        e0->data.F32[i] = pol.e0;
+        e1->data.F32[i] = pol.e1;
+        e2->data.F32[i] = pol.e2;
+
+	float flux = source->modelEXT->params->data.F32[PM_PAR_I0];
+	mag->data.F32[i] = (flux > 0.0) ? -2.5*log(flux) : -100.0;
+    }
+
+    if (psf->psfTrendMode == PM_TREND_MAP) {
+	float errorFloor = 0.0;
+	float errorTotal = 0.0;
+	float errorTotalMin = FLT_MAX;
+	int entryMin = -1;
+
+	psVector *dz = NULL;
+	psVector *mask = psVectorAlloc (sources->n, PS_TYPE_U8);
+
+	// check the fit residuals and increase Nx,Ny until the error is minimized
+	// pmPSFParamTrend increases the number along the longer of x or y
+	for (int i = 1; i < PS_MAX (psf->trendNx, psf->trendNy); i++) {
+
+	    psVectorInit (mask, 0);
+	    if (!pmPSFFitShapeParamsMap (psf, i, &errorFloor, &errorTotal, mask, x, y, mag, e0, e1, e2, dz)) {
+		break;
+	    }
+
+	    // we do not have a good model for the error distributions on e0, e1, e2. 
+	    // use the bright end scatter from the constant fit to set the scale for the 
+	    // versions with 2D variation.  potentially scale by poisson errors...
+	    if (i == 1) {
+		// if (psf->poissonErrorsParams) do something else..
+		dz = psVectorAlloc (sources->n, PS_TYPE_F32);
+		for (int i = 0; i < sources->n; i++) {
+		    dz->data.F32[i] = errorFloor;
+		}
+	    }
+
+	    // store the resulting errorTotal values and the scales, redo the best
+	    if (errorTotal < errorTotalMin) {
+		errorTotalMin = errorTotal;
+		entryMin = i;
+	    }
+	}
+	if (entryMin == -1) {
+	    psError (PS_ERR_UNKNOWN, true, "failed to find image map for shape params");
+	    return false;
+	}
+
+	// XXX supply the resulting mask values back to srcMask
+	psVectorInit (mask, 0);
+	if (!pmPSFFitShapeParamsMap (psf, entryMin, &errorFloor, &errorTotal, mask, x, y, mag, e0, e1, e2, dz)) {
+	    psAbort ("failed pmPSFFitShapeParamsMap on second pass?");
+	}
+       
+	pmTrend2D *trend = psf->params->data[PM_PAR_E0];
+	psf->trendNx = trend->map->map->numCols;
+	psf->trendNy = trend->map->map->numRows;
+
+	psFree (mask);
+	psFree (dz);
+    } else {
+
+	// XXX iterate Nx, Ny based on scatter?
+	// XXX we force the x & y order to be the same
+	// modify the order to correspond to the actual number of matched stars:
+	int order = PS_MAX (psf->trendNx, psf->trendNy);
+	if ((sources->n < 15) && (order >= 3)) order = 2;
+	if ((sources->n < 11) && (order >= 2)) order = 1;
+	if ((sources->n <  8) && (order >= 1)) order = 0;
+	if ((sources->n <  3)) {
+	    psError (PS_ERR_UNKNOWN, true, "failed to fit polynomial to shape params");
+	    return false;
+	}
+	psf->trendNx = order;
+	psf->trendNy = order;
+
+	// we run 'clipIter' cycles clipping in each of x and y, with only one iteration each.
+	// This way, the parameters masked by one of the fits will be applied to the others
+	for (int i = 0; i < psf->psfTrendStats->clipIter; i++) {
+
+	    // XXX we are using the same stats structure on each pass: do we need to re-init it?
+	    bool status = true;
+	    status &= pmTrend2DFit (psf->params->data[PM_PAR_E0], srcMask, 0xff, x, y, e0, NULL);
+	    psTrace ("psModules.pmPSFtry", 4, "clipped E0 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e0->n);
+	    status &= pmTrend2DFit (psf->params->data[PM_PAR_E1], srcMask, 0xff, x, y, e1, NULL);
+	    psTrace ("psModules.pmPSFtry", 4, "clipped E1 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e1->n);
+	    status &= pmTrend2DFit (psf->params->data[PM_PAR_E2], srcMask, 0xff, x, y, e2, NULL);
+	    psTrace ("psModules.pmPSFtry", 4, "clipped E2 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e2->n);
+
+	    if (!status) {
+		psError (PS_ERR_UNKNOWN, true, "failed to fit polynomial to shape params");
+		return false;
+	    }
+	}
+    }
+
+    // test dump of the psf parameters
+    if (psTraceGetLevel("psModules.objects") >= 4) {
+        FILE *f = fopen ("pol.dat", "w");
+	fprintf (f, "# x y  :  e0obs e1obs e2obs  : e0fit e1fit e2fit : mask\n");
+        for (int i = 0; i < e0->n; i++) {
+            fprintf (f, "%f %f  :  %f %f %f  : %f %f %f  : %d\n",
+                     x->data.F32[i], y->data.F32[i],
+                     e0->data.F32[i], e1->data.F32[i], e2->data.F32[i], 
+                     pmTrend2DEval (psf->params->data[PM_PAR_E0], x->data.F32[i], y->data.F32[i]),
+                     pmTrend2DEval (psf->params->data[PM_PAR_E1], x->data.F32[i], y->data.F32[i]),
+                     pmTrend2DEval (psf->params->data[PM_PAR_E2], x->data.F32[i], y->data.F32[i]),
+		     srcMask->data.U8[i]);
+        }
+        fclose (f);
+    }
+
+    psFree (e0);
+    psFree (e1);
+    psFree (e2);
+    psFree (mag);
+    return true;
+}
+
+// fit the shape variations as a psImageMap for the given scale factor
+bool pmPSFFitShapeParamsMap (pmPSF *psf, int scale, float *errorFloor, float *errorTotal, psVector *mask, psVector *x, psVector *y, psVector *mag, psVector *e0obs, psVector *e1obs, psVector *e2obs, psVector *dz) {
+
+    int Nx, Ny;
+	
+    // set the map scale to match the aspect ratio
+    if (psf->fieldNx > psf->fieldNy) {
+	Nx = scale;
+	Ny = PS_MAX (1, Nx * (psf->fieldNy / psf->fieldNx));
+    } else {
+	Ny = scale;
+	Nx = PS_MAX (1, Ny * (psf->fieldNx / psf->fieldNy));
+    }
+
+    // do we have enough sources for this fine of a grid?
+    if (x->n < 3*Nx*Ny) {
+	return false;
+    }
+
+    // the mask marks the values not used to calculate the ApTrend
+    psVectorInit (mask, 0);
+
+    // XXX check this against the exising type
+    pmTrend2DMode psfTrendMode = PM_TREND_MAP;
+
+    psImageBinning *binning = psImageBinningAlloc();
+    binning->nXruff = Nx;
+    binning->nYruff = Ny;
+    binning->nXfine = psf->fieldNx;
+    binning->nYfine = psf->fieldNy;
+    psImageBinningSetScale (binning, PS_IMAGE_BINNING_CENTER);
+    psImageBinningSetSkipByOffset (binning, psf->fieldXo, psf->fieldYo);
+
+    psFree (psf->params->data[PM_PAR_E0]);
+    psFree (psf->params->data[PM_PAR_E1]);
+    psFree (psf->params->data[PM_PAR_E2]);
+
+    int nIter = psf->psfTrendStats->clipIter;
+    psf->psfTrendStats->clipIter = 1;
+    psf->params->data[PM_PAR_E0] = pmTrend2DNoImageAlloc (psfTrendMode, binning, psf->psfTrendStats);
+    psf->params->data[PM_PAR_E1] = pmTrend2DNoImageAlloc (psfTrendMode, binning, psf->psfTrendStats);
+    psf->params->data[PM_PAR_E2] = pmTrend2DNoImageAlloc (psfTrendMode, binning, psf->psfTrendStats);
+    psFree (binning);
+
+    // we run 'clipIter' cycles clipping in each of x and y, with only one iteration each.
+    // This way, the parameters masked by one of the fits will be applied to the others
+    for (int i = 0; i < nIter; i++) {
+	// XXX we are using the same stats structure on each pass: do we need to re-init it?
+        pmTrend2DFit (psf->params->data[PM_PAR_E0], mask, 0xff, x, y, e0obs, dz);
+        psTrace ("psModules.objects", 5, "clipped E0 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e0obs->n);
+        pmTrend2DFit (psf->params->data[PM_PAR_E1], mask, 0xff, x, y, e1obs, dz);
+        psTrace ("psModules.objects", 5, "clipped E1 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e1obs->n);
+        pmTrend2DFit (psf->params->data[PM_PAR_E2], mask, 0xff, x, y, e2obs, dz);
+        psTrace ("psModules.objects", 5, "clipped E2 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e2obs->n);
+    }
+    psf->psfTrendStats->clipIter = nIter; // restore default setting
+
+    // construct the fitted values and the residuals
+    psVector *e0fit = pmTrend2DEvalVector (psf->params->data[PM_PAR_E0], x, y);
+    psVector *e1fit = pmTrend2DEvalVector (psf->params->data[PM_PAR_E1], x, y);
+    psVector *e2fit = pmTrend2DEvalVector (psf->params->data[PM_PAR_E2], x, y);
+
+    psVector *e0res = (psVector *) psBinaryOp (NULL, (void *) e0obs, "-", (void *) e0fit);
+    psVector *e1res = (psVector *) psBinaryOp (NULL, (void *) e1obs, "-", (void *) e1fit);
+    psVector *e2res = (psVector *) psBinaryOp (NULL, (void *) e2obs, "-", (void *) e2fit);
+
+    // measure errors on the map pixels
+    pmTrend2D *trend;
+
+    trend = psf->params->data[PM_PAR_E0];
+    float mapErrorSum = 0.0;
+    float mapError = 0.0;
+    for (int iy = 0; iy < trend->map->error->numRows; iy++) { 
+	for (int ix = 0; ix < trend->map->error->numCols; ix++) { 
+	    mapError += PS_SQR (trend->map->error->data.F32[iy][ix]);
+	}
+    }
+    psTrace ("psModules.objects", 4, "E0 error: %f\n", sqrt(mapError));
+    mapErrorSum += mapError;
+
+    trend = psf->params->data[PM_PAR_E1];
+    mapError = 0.0;
+    for (int iy = 0; iy < trend->map->error->numRows; iy++) { 
+	for (int ix = 0; ix < trend->map->error->numCols; ix++) { 
+	    mapError += PS_SQR (trend->map->error->data.F32[iy][ix]);
+	}
+    }
+    psTrace ("psModules.objects", 4, "E1 error: %f\n", sqrt(mapError));
+    mapErrorSum += mapError;
+
+    trend = psf->params->data[PM_PAR_E2];
+    mapError = 0.0;
+    for (int iy = 0; iy < trend->map->error->numRows; iy++) { 
+	for (int ix = 0; ix < trend->map->error->numCols; ix++) { 
+	    mapError += PS_SQR (trend->map->error->data.F32[iy][ix]);
+	}
+    }
+    psTrace ("psModules.objects", 4, "E2 error: %f\n", sqrt(mapError));
+    mapErrorSum += mapError;
+
+    psTrace ("psModules.objects", PS_LOG_INFO, "total map error: %f\n", sqrt(mapErrorSum));
+
+    // measure systematic errorFloor & systematic / photon scale factor
+    // XXX this is a bit arbitrary, but it forces ~3 stars from the bright bin per spatial bin
+    int nGroup = PS_MAX (3*Nx*Ny, 10);
+    pmPSFShapeParamsErrors (errorFloor, mag, e0res, e1res, e2res, mask, nGroup);
+
+    *errorTotal = sqrt(PS_SQR(*errorFloor) + mapErrorSum);
+
+    psLogMsg ("psphot.psftry", PS_LOG_INFO, "result of %d x %d grid (%d stars per bin)\n", Nx, Ny, nGroup);
+    psLogMsg ("psphot.psftry", PS_LOG_INFO, "systematic scatter floor: %f, error map total: %f\n", *errorFloor, *errorTotal);
+
+    psFree (e0fit);
+    psFree (e1fit);
+    psFree (e2fit);
+
+    psFree (e0res);
+    psFree (e1res);
+    psFree (e2res);
+
+    return true;
+}
+
+// calculate the minimum scatter of the parameters
+bool pmPSFShapeParamsErrors (float *errorFloor, psVector *mag, psVector *e0res, psVector *e1res, psVector *e2res, psVector *mask, int nGroup) {
+
+    psStats *statsS = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+
+    // measure the trend in bins with 10 values each; if < 10 total, use them all
+    int nBin = PS_MAX (mag->n / nGroup, 1);
+
+    // output vectors for ApResid trend 
+    psVector *dSo = psVectorAlloc (nBin, PS_TYPE_F32);
+
+    // use mag to group parameters in sequence
+    psVector *index = psVectorSortIndex (NULL, mag);
+
+    // subset vectors for mag and dap values within the given range
+    psVector *dE0subset = psVectorAllocEmpty (nGroup, PS_TYPE_F32);
+    psVector *dE1subset = psVectorAllocEmpty (nGroup, PS_TYPE_F32);
+    psVector *dE2subset = psVectorAllocEmpty (nGroup, PS_TYPE_F32);
+    psVector *mkSubset  = psVectorAllocEmpty (nGroup, PS_TYPE_U8);
+
+    int n = 0;
+    for (int i = 0; i < dSo->n; i++) {
+	int j;
+	for (j = 0; (j < nGroup) && (n < mag->n); j++, n++) {
+	    int N = index->data.U32[n];
+	    dE0subset->data.F32[j] = e0res->data.F32[N];
+	    dE1subset->data.F32[j] = e1res->data.F32[N];
+	    dE2subset->data.F32[j] = e2res->data.F32[N];
+
+	    mkSubset->data.U8[j]   = mask->data.U8[N];
+	}
+	dE0subset->n = j;
+	dE1subset->n = j;
+	dE2subset->n = j;
+	mkSubset->n = j;
+
+	// calculate the root-mean-square of E0, E1, E2
+	float dEsquare = 0.0;
+	psStatsInit (statsS);
+	psVectorStats (statsS, dE0subset, NULL, mkSubset, 0xff);
+	dEsquare += PS_SQR(statsS->robustStdev);
+
+	psStatsInit (statsS);
+	psVectorStats (statsS, dE1subset, NULL, mkSubset, 0xff);
+	dEsquare += PS_SQR(statsS->robustStdev);
+
+	psStatsInit (statsS);
+	psVectorStats (statsS, dE2subset, NULL, mkSubset, 0xff);
+	dEsquare += PS_SQR(statsS->robustStdev);
+
+	dSo->data.F32[i] = sqrt(dEsquare);
+    }
+    psFree (dE0subset);
+    psFree (dE1subset);
+    psFree (dE2subset);
+    psFree (mkSubset);
+    
+    psStats *stats = psStatsAlloc (PS_STAT_MIN);
+    psVectorStats (stats, dSo, NULL, NULL, 0);
+
+    *errorFloor = stats->min;
+
+    psFree (stats);
+    psFree (index);
+
+    psFree (dSo);
+    
+    psFree (statsS);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmPSFtry.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmPSFtry.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmPSFtry.h	(revision 15358)
@@ -0,0 +1,132 @@
+/* @file  pmPSFtry.h
+ *
+ * This file contains code that allows the user to try to fit several
+ * PSF models to an image.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.16 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-09-25 22:05:05 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_PSF_TRY_H
+# define PM_PSF_TRY_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/**
+ *
+ * This structure contains a pointer to the collection of sources which will
+ * be used to test the PSF model form. It lists the pmModelType type of model
+ * being tests, and contains an element to store the resulting psf
+ * representation. In addition, this structure carries the complete collection of
+ * EXT (floating parameter) and PSF (fixed parameter) model fits to each of the
+ * sources modelEXT and modelPSF. It also contains a mask which is set by the
+ * model fitting and psf fitting steps. For each model, the value of the quality
+ * metric is stored in the vector metric and the fitted instrumental magnitude is
+ * stored in fitMag. The quality metric for the PSF model is the aperture
+ * magnitude minus the fitted magnitude for each source. This collection of
+ * aperture residuals is examined in the analysis process, and a linear trend of
+ * the residual with the inverse object flux (ie, 100:4his structure contains a
+ * pointer to the collection of sources which will be used to test the PSF model
+ * form. It lists the pmModelType type of modmag) is fitted. The result of this
+ * fit is a measured sky bias (systematic error in the sky measured by the fits),
+ * an effective infinite-magnitude aperture correction (ApResid), and the scatter
+ * of the aperture correction for the ensemble of PSF stars (dApResid). The
+ * ultimate metric to intercompare multiple types of PSF models is the value of
+ * the aperture correction scatter.
+ *
+ * XXX: There are many more members in the SDRS then in the prototype code.
+ * I stuck with the prototype code.
+ *
+ *
+ */
+typedef struct
+{
+    pmPSF      *psf;                    ///< Add comment.
+    psArray    *sources;                ///< pointers to the original sources
+    psVector   *mask;                   ///< Add comment.
+    psVector   *metric;                 ///< Add comment.
+    psVector   *metricErr;              ///< Add comment.
+    psVector   *fitMag;                 ///< Add comment.
+}
+pmPSFtry;
+
+
+/** pmPSFtryMaskValues
+ *
+ * The following datatype defines the masks used by the pmPSFtry analysis to
+ * identify sources which should or should not be included in the analysis.
+ *
+ */
+typedef enum {
+    PSFTRY_MASK_CLEAR    = 0x00,        ///< Add comment.
+    PSFTRY_MASK_OUTLIER  = 0x01,        ///< 1: outlier in psf polynomial fit (provided by psPolynomials)
+    PSFTRY_MASK_EXT_FAIL = 0x02,        ///< 2: ext model failed to converge
+    PSFTRY_MASK_PSF_FAIL = 0x04,        ///< 3: psf model failed to converge
+    PSFTRY_MASK_BAD_PHOT = 0x08,        ///< 4: invalid source photometry
+    PSFTRY_MASK_BAD_MODEL= 0x10,        ///< 5: could not build PSF from EXT (!??)
+    PSFTRY_MASK_ALL      = 0x1f,        ///< Add comment.
+} pmPSFtryMaskValues;
+
+
+/** pmPSFtryAlloc()
+ *
+ * Allocate a pmPSFtry data structure.
+ *
+ */
+pmPSFtry *pmPSFtryAlloc (psArray *sources, pmPSFOptions *options);
+
+/** pmPSFtryModel()
+ *
+ * This function takes the input collection of sources and performs a complete
+ * analysis to determine a PSF model of the given type (specified by model name).
+ * The result is a pmPSFtry with the results of the analysis.
+ *
+ */
+pmPSFtry *pmPSFtryModel (psArray *sources, char *modelName, pmPSFOptions *options, psMaskType maskVal, psMaskType mark);
+
+/** pmPSFtryMetric()
+ *
+ * This function is used to measure the PSF model metric for the set of
+ * results contained in the pmPSFtry structure.
+ *
+ */
+bool pmPSFtryMetric(
+    pmPSFtry *psfTry,                  ///< Add comment.
+    float RADIUS                       ///< Add comment.
+);
+
+/** pmPSFtryMetric_Alt()
+ *
+ * This function is used to measure the PSF model metric for the set of
+ * results contained in the pmPSFtry structure (alternative implementation).
+ *
+ */
+bool pmPSFtryMetric_Alt(
+    pmPSFtry *try,                      ///< Add comment.
+    float RADIUS                        ///< Add comment.
+);
+
+/**
+ *
+ * This function takes a collection of pmModel fitted models from across a
+ * single image and builds a pmPSF representation of the PSF. The input array of
+ * model fits may consist of entries to be ignored (noted by a non-zero mask
+ * entry). The analysis of the models fits a 2D polynomial for each parameter to
+ * the collection of model parameters as a function of position (and
+ * normalization?). In this process, some of the input models may be marked as
+ * outliers and excluded from the fit. These elements will be marked with a
+ * specific mask value (1 == PSFTRY_MASK_OUTLIER).
+ *
+ */
+bool pmPSFFromPSFtry (pmPSFtry *psfTry);
+
+bool pmPSFFitShapeParams (pmPSF *psf, psArray *sources, psVector *x, psVector *y, psVector *srcMask);
+bool pmPSFFitShapeParamsMap (pmPSF *psf, int scale, float *errorFloor, float *errorTotal, psVector *mask, psVector *x, psVector *y, psVector *mag, psVector *e0obs, psVector *e1obs, psVector *e2obs, psVector *dz);
+bool pmPSFShapeParamsErrors (float *errorFloor, psVector *mag, psVector *e0res, psVector *e1res, psVector *e2res, psVector *mask, int nGroup);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/objects/pmPeaks.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmPeaks.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmPeaks.c	(revision 15358)
@@ -0,0 +1,564 @@
+/** @file  pmPeaks.c
+ *
+ *  This file defines functions to detect and manipulate peaks in images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.17 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-27 03:35:29 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmPeaks.h"
+
+/******************************************************************************
+AddPeak(): A private function which allocates a psArray, if the peaks
+argument is NULL, otherwise it adds the peak to that array.
+XXX EAM : row,col now refer to image coords, NOT parent (since this is private) 
+XXX EAM : now also calculates fractional peak positions from 3x3 bicube region
+*****************************************************************************/
+static psArray *AddPeak(psArray *peaks,
+                        const psImage *image,
+                        psS32 row,
+                        psS32 col,
+                        pmPeakType type)
+{
+    psTrace("psModules.objects", 5, "---- begin ----\n");
+
+    if (peaks == NULL) {
+        peaks = psArrayAllocEmpty(100);
+    }
+
+    // the peak position is in parent coordinates
+    pmPeak *peak = pmPeakAlloc(col + image->col0, row + image->row0, image->data.F32[row][col], type);
+
+    // measure fractional peak position using the 3x3 bicube fit
+
+    // ix,iy must land on image with 1 pixel border
+    int ix = PS_MAX (PS_MIN (col, image->numCols - 2), 1);
+    int iy = PS_MAX (PS_MIN (row, image->numRows - 2), 1);
+
+    // calculate peak position relative to ix,iy
+    // XXX these functions need to take a mask, weight, and calculate the errors
+    psPolynomial2D *bicube = psImageBicubeFit (image, ix + image->col0, iy + image->row0);
+    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 + ix + image->col0;
+        peak->yf = min.y + iy + image->row0;
+	peak->dx = 0.0;
+	peak->dy = 0.0;
+    } else {
+        peak->xf = ix;
+        peak->yf = iy;
+	peak->dx = 1.0;
+	peak->dy = 1.0;
+    }
+
+    psArrayAdd(peaks, 100, peak);
+    psFree (peak);
+
+    psTrace("psModules.objects", 5, "---- end ----\n");
+    return(peaks);
+}
+
+/******************************************************************************
+getRowVectorFromImage(): a private function which simply returns a
+psVector containing the specified row of data from the psImage.
+ 
+XXX: Is there a better way to do this?
+XXX EAM: does this really need to alloc a new vector???
+*****************************************************************************/
+static psVector *getRowVectorFromImage(psImage *image,
+                                       psU32 row)
+{
+    psTrace("psModules.objects", 4, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_IMAGE_NON_NULL(image, NULL);
+    PS_ASSERT_IMAGE_TYPE(image, PS_TYPE_F32, NULL);
+
+    psVector *tmpVector = psVectorAlloc(image->numCols, PS_TYPE_F32);
+    for (psU32 col = 0; col < image->numCols ; col++) {
+        tmpVector->data.F32[col] = image->data.F32[row][col];
+    }
+    psTrace("psModules.objects", 4, "---- %s() end ----\n", __func__);
+    return(tmpVector);
+}
+
+/******************************************************************************
+isItInThisRegion(): a private function which simply returns a
+boolean denoting if specified coordinate is in the region.
+XXX: Macro this.
+*****************************************************************************/
+static bool isItInThisRegion(const psRegion valid,
+                             psS32 x,
+                             psS32 y)
+{
+    psTrace("psModules.objects", 4, "---- %s() begin ----\n", __func__);
+    if ((x >= valid.x0) &&
+            (x <= valid.x1) &&
+            (y >= valid.y0) &&
+            (y <= valid.y1)) {
+        psTrace("psModules.objects", 4, "---- %s(true) end ----\n", __func__);
+        return(true);
+    }
+    psTrace("psModules.objects", 4, "---- %s(false) end ----\n", __func__);
+    return(false);
+}
+
+/******************************************************************************
+pmPeakAlloc(): Allocate the pmPeak data structure and set appropriate members.
+*****************************************************************************/
+static void peakFree(pmPeak *tmp)
+{} // used by pmPeakTest()
+
+pmPeak *pmPeakAlloc(psS32 x,
+                    psS32 y,
+                    psF32 value,
+                    pmPeakType type)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    static int id = 1;
+    pmPeak *tmp = (pmPeak *) psAlloc(sizeof(pmPeak));
+    *(int *)&tmp->id = id++;
+    tmp->x = x;
+    tmp->y = y;
+    tmp->value = value;
+    tmp->flux = 0;
+    tmp->SN = 0;
+    tmp->xf = x;
+    tmp->yf = y;
+
+    tmp->type = type;
+
+    psMemSetDeallocator(tmp, (psFreeFunc) peakFree);
+
+    psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+    return(tmp);
+}
+
+bool pmPeakTest(const psPtr ptr)
+{
+    return (psMemGetDeallocator(ptr) == (psFreeFunc)peakFree);
+}
+
+// psSort comparison function for peaks
+int pmPeaksCompareAscend (const void **a, const void **b)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 diff;
+
+    diff = A->value - B->value;
+    if (diff < FLT_EPSILON) {
+        psTrace("psModules.objects", 3, "---- %s(-1) end ----\n", __func__);
+        return (-1);
+    } else if (diff > FLT_EPSILON) {
+        psTrace("psModules.objects", 3, "---- %s(+1) end ----\n", __func__);
+        return (+1);
+    }
+    psTrace("psModules.objects", 3, "---- %s(0) end ----\n", __func__);
+    return (0);
+}
+
+// psSort comparison function for peaks
+int pmPeaksCompareDescend (const void **a, const void **b)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 diff;
+
+    diff = A->value - B->value;
+    if (diff < FLT_EPSILON) {
+        psTrace("psModules.objects", 3, "---- %s(+1) end ----\n", __func__);
+        return (+1);
+    } else if (diff > FLT_EPSILON) {
+        psTrace("psModules.objects", 3, "---- %s(-1) end ----\n", __func__);
+        return (-1);
+    }
+    psTrace("psModules.objects", 3, "---- %s(0) end ----\n", __func__);
+    return (0);
+}
+
+// sort by SN (descending)
+int pmPeakSortBySN (const void **a, const void **b)
+{
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 fA = A->SN;
+    psF32 fB = B->SN;
+    if (isnan (fA)) fA = 0;
+    if (isnan (fB)) fB = 0;
+
+    psF32 diff = fA - fB;
+    if (diff > FLT_EPSILON) return (-1);
+    if (diff < FLT_EPSILON) return (+1);
+    return (0);
+}
+
+// sort by Y (ascending)
+int pmPeakSortByY (const void **a, const void **b)
+{
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 fA = A->y;
+    psF32 fB = B->y;
+
+    psF32 diff = fA - fB;
+    if (diff > FLT_EPSILON) return (+1);
+    if (diff < FLT_EPSILON) return (-1);
+    return (0);
+}
+
+/******************************************************************************
+pmPeaksInVector(vector, threshold): Find all local peaks in the given vector
+above the given threshold.  Returns a vector of type PS_TYPE_U32 containing
+the location (x value) of all peaks.
+ 
+XXX: What types should be supported?  Only F32 is implemented.
+ 
+XXX: We currently step through the input vector twice; once to determine the
+size of the output vector, then to set the values of the output vector.
+Depending upon actual use, this may need to be optimized.
+*****************************************************************************/
+psVector *pmPeaksInVector(const psVector *vector,
+			 psF32 threshold)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_VECTOR_NON_NULL(vector, NULL);
+    PS_ASSERT_VECTOR_NON_EMPTY(vector, NULL);
+    PS_ASSERT_VECTOR_TYPE(vector, PS_TYPE_F32, NULL);
+    int count = 0;
+    int n = vector->n;
+
+    //
+    // Special case: the input vector has a single element.
+    //
+    if (n == 1) {
+        psVector *tmpVector = NULL;
+        if (vector->data.F32[0] > threshold) {
+            tmpVector = psVectorAlloc(1, PS_TYPE_U32);
+            tmpVector->data.U32[0] = 0;
+        } else {
+            tmpVector = psVectorAlloc(0, PS_TYPE_U32);
+        }
+        psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+        return(tmpVector);
+    }
+
+    //
+    // Determine if first pixel is a peak
+    //
+    if ((vector->data.F32[0] > vector->data.F32[1]) &&
+            (vector->data.F32[0] > threshold)) {
+        count++;
+    }
+
+    //
+    // Determine if interior pixels are peaks
+    //
+    for (psU32 i = 1; i < n-1 ; i++) {
+        if ((vector->data.F32[i] > vector->data.F32[i-1]) &&
+                (vector->data.F32[i] >= vector->data.F32[i+1]) &&
+                (vector->data.F32[i] > threshold)) {
+            count++;
+        }
+    }
+
+    //
+    // Determine if last pixel is a peak
+    //
+    if ((vector->data.F32[n-1] > vector->data.F32[n-2]) &&
+            (vector->data.F32[n-1] > threshold)) {
+        count++;
+    }
+
+    //
+    // We know how many peaks exist, so we now allocate a psVector to store
+    // those peaks.
+    //
+    psVector *tmpVector = psVectorAlloc(count, PS_TYPE_U32);
+    count = 0;
+
+    //
+    // Determine if first pixel is a peak
+    //
+    if ((vector->data.F32[0] > vector->data.F32[1]) &&
+            (vector->data.F32[0] > threshold)) {
+        tmpVector->data.U32[count++] = 0;
+    }
+
+    //
+    // Determine if interior pixels are peaks
+    //
+    for (psU32 i = 1; i < (n-1) ; i++) {
+        if ((vector->data.F32[i] > vector->data.F32[i-1]) &&
+                (vector->data.F32[i] >= vector->data.F32[i+1]) &&
+                (vector->data.F32[i] > threshold)) {
+            tmpVector->data.U32[count++] = i;
+        }
+    }
+
+    //
+    // Determine if last pixel is a peak
+    //
+    if ((vector->data.F32[n-1] > vector->data.F32[n-2]) &&
+            (vector->data.F32[n-1] > threshold)) {
+        tmpVector->data.U32[count++] = n-1;
+    }
+
+    psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+    return(tmpVector);
+}
+
+
+/******************************************************************************
+pmPeaksInImage(image, threshold): Find all local peaks in the given psImage
+above the given threshold.  Returns a psArray containing location (x/y value)
+of all peaks.
+ 
+XXX: I'm not convinced the peak type definition in the SDRS is mutually
+exclusive.  Some peaks can have multiple types.  Edges for sure.  Also, a
+digonal line with the same value at each point will have a peak for every
+point on that line.
+ 
+XXX: This does not work if image has either a single row, or a single column.
+ 
+The peak is returned in the image parent coordinates
+
+*****************************************************************************/
+psArray *pmPeaksInImage(const psImage *image, psF32 threshold)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_IMAGE_NON_NULL(image, NULL);
+    PS_ASSERT_IMAGE_TYPE(image, PS_TYPE_F32, NULL);
+    if ((image->numRows == 1) || (image->numCols == 1)) {
+        psError(PS_ERR_UNKNOWN, true, "Currently, input image must have at least 2 rows and 2 columns.");
+        psTrace("psModules.objects", 3, "---- %s(NULL) end ----\n", __func__);
+        return(NULL);
+    }
+    psVector *tmpRow = NULL;
+    psU32 col = 0;
+    psU32 row = 0;
+    psArray *list = NULL;
+
+    // Find peaks in row 0 only.
+    row = 0;
+    tmpRow = getRowVectorFromImage((psImage *) image, row);
+    psVector *row1 = pmPeaksInVector(tmpRow, threshold);
+    // pmPeaksInVector returns coords in the vector, not corrected for col0
+
+    for (psU32 i = 0 ; i < row1->n ; i++ ) {
+        col = row1->data.U32[i];
+        // is pixel (0,0) is a peak?
+        if (col == 0) {
+            if ( (image->data.F32[row][col] >  image->data.F32[row][col+1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row+1][col]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row+1][col+1])) {
+
+                if (image->data.F32[row][col] > threshold) {
+                    list = AddPeak(list, image, row, col, PM_PEAK_EDGE);
+                }
+            }
+        } else if (col < (image->numCols - 1)) {
+            if ( (image->data.F32[row][col] >= image->data.F32[row][col-1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row][col+1]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row+1][col-1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row+1][col]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row+1][col+1])) {
+                if (image->data.F32[row][col] > threshold) {
+                    list = AddPeak(list, image, row, col, PM_PEAK_EDGE);
+                }
+            }
+
+        } else if (col == (image->numCols - 1)) {
+            if ( (image->data.F32[row][col] >= image->data.F32[row][col-1]) &&
+                    (image->data.F32[row][col] > image->data.F32[row+1][col]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row+1][col-1])) {
+                if (image->data.F32[row][col] > threshold) {
+                    list = AddPeak(list, image, row, col, PM_PEAK_EDGE);
+                }
+            }
+
+        } else {
+            psError(PS_ERR_UNKNOWN, true, "peak specified valid column range.");
+        }
+    }
+    psFree (tmpRow);
+    psFree (row1);
+
+    //
+    // Exit if this image has a single row.
+    //
+    if (image->numRows == 1) {
+        psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+        return(list);
+    }
+
+    //
+    // Find peaks in interior rows only.
+    //
+    for (row = 1 ; row < (image->numRows - 1) ; row++) {
+        tmpRow = getRowVectorFromImage((psImage *) image, row);
+        row1 = pmPeaksInVector(tmpRow, threshold);
+
+        // Step through all local peaks in this row.
+        for (psU32 i = 0 ; i < row1->n ; i++ ) {
+            pmPeakType myType = PM_PEAK_UNDEF;
+            col = row1->data.U32[i];
+
+            if (col == 0) {
+                // If col==0, then we can not read col-1 pixels
+                if ((image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row-1][col+1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row][col+1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col+1])) {
+                    myType = PM_PEAK_EDGE;
+                    list = AddPeak(list, image, row, col, myType);
+                }
+            } else if (col < (image->numCols - 1)) {
+                // This is an interior pixel
+                if ((image->data.F32[row][col] >= image->data.F32[row-1][col-1]) &&
+                        (image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row-1][col+1]) &&
+                        (image->data.F32[row][col] > image->data.F32[row][col-1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row][col+1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col-1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col+1])) {
+                    if (image->data.F32[row][col] > threshold) {
+                        if ((image->data.F32[row][col] > image->data.F32[row-1][col-1]) &&
+                                (image->data.F32[row][col] > image->data.F32[row-1][col]) &&
+                                (image->data.F32[row][col] > image->data.F32[row-1][col+1]) &&
+                                (image->data.F32[row][col] > image->data.F32[row][col-1]) &&
+                                (image->data.F32[row][col] > image->data.F32[row][col+1]) &&
+                                (image->data.F32[row][col] > image->data.F32[row+1][col-1]) &&
+                                (image->data.F32[row][col] > image->data.F32[row+1][col]) &&
+                                (image->data.F32[row][col] > image->data.F32[row+1][col+1])) {
+                            myType = PM_PEAK_LONE;
+                        }
+
+                        if ((image->data.F32[row][col] == image->data.F32[row-1][col-1]) ||
+                                (image->data.F32[row][col] == image->data.F32[row-1][col]) ||
+                                (image->data.F32[row][col] == image->data.F32[row-1][col+1]) ||
+                                (image->data.F32[row][col] == image->data.F32[row][col-1]) ||
+                                (image->data.F32[row][col] == image->data.F32[row][col+1]) ||
+                                (image->data.F32[row][col] == image->data.F32[row+1][col-1]) ||
+                                (image->data.F32[row][col] == image->data.F32[row+1][col]) ||
+                                (image->data.F32[row][col] == image->data.F32[row+1][col+1])) {
+                            myType = PM_PEAK_FLAT;
+                        }
+
+                        list = AddPeak(list, image, row, col, myType);
+
+                    }
+                }
+            } else if (col == (image->numCols - 1)) {
+                // If col==numCols - 1, then we can not read col+1 pixels
+                if ((image->data.F32[row][col] >= image->data.F32[row-1][col-1]) &&
+                        (image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                        (image->data.F32[row][col] > image->data.F32[row][col-1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row][col+1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col-1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col])) {
+                    myType = PM_PEAK_EDGE;
+                    list = AddPeak(list, image, row, col, myType);
+                }
+            } else {
+                psError(PS_ERR_UNKNOWN, true, "peak specified outside valid column range.");
+            }
+
+        }
+        psFree (tmpRow);
+        psFree (row1);
+    }
+
+    //
+    // Find peaks in the last row only.
+    //
+    row = image->numRows - 1;
+    tmpRow = getRowVectorFromImage((psImage *) image, row);
+    row1 = pmPeaksInVector(tmpRow, threshold);
+    for (psU32 i = 0 ; i < row1->n ; i++ ) {
+        col = row1->data.U32[i];
+        if (col == 0) {
+            if ( (image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row-1][col+1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row][col+1])) {
+                if (image->data.F32[row][col] > threshold) {
+                    list = AddPeak(list, image, row, col, PM_PEAK_EDGE);
+                }
+            }
+        } else if (col < (image->numCols - 1)) {
+            if ( (image->data.F32[row][col] >= image->data.F32[row-1][col-1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row-1][col+1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row][col-1]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row][col+1])) {
+                if (image->data.F32[row][col] > threshold) {
+                    list = AddPeak(list, image, row, col, PM_PEAK_EDGE);
+                }
+            }
+
+        } else if (col == (image->numCols - 1)) {
+            if ( (image->data.F32[row][col] >= image->data.F32[row-1][col-1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row][col-1])) {
+                if (image->data.F32[row][col] > threshold) {
+                    list = AddPeak(list, image, row, col, PM_PEAK_EDGE);
+                }
+            }
+        } else {
+            psError(PS_ERR_UNKNOWN, true, "peak specified outside valid column range.");
+        }
+    }
+    psFree (tmpRow);
+    psFree (row1);
+    psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+    return(list);
+}
+
+// return a new array of peaks which are in the valid region and below threshold
+// XXX this function is unused and probably could be dropped
+psArray *pmPeaksSubset(
+    psArray *peaks,
+    psF32 maxValue,
+    const psRegion valid)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(peaks, NULL);
+
+    psArray *output = psArrayAllocEmpty (200);
+
+    psTrace ("psModules.objects", 3, "list size is %ld\n", peaks->n);
+
+    for (int i = 0; i < peaks->n; i++) {
+        pmPeak *tmpPeak = (pmPeak *) peaks->data[i];
+        if (tmpPeak->value > maxValue)
+            continue;
+        if (isItInThisRegion(valid, tmpPeak->x, tmpPeak->y))
+            continue;
+        psArrayAdd (output, 200, tmpPeak);
+    }
+    psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+    return(output);
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmPeaks.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmPeaks.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmPeaks.h	(revision 15358)
@@ -0,0 +1,156 @@
+/* @file  pmPeaks.h
+ *
+ * The process of finding, measuring, and classifying astronomical sources on
+ * images is one of the critical tasks of the IPP or any astronomical software
+ * system. This file will define structures and functions related to the task
+ * of source detection and measurement. The elements defined in this section
+ * are generally low-level components which can be connected together to
+ * construct a complete object measurement suite.
+ *
+ * @author GLG, MHPCC
+ *
+ * @version $Revision: 1.8 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-09-27 03:35:29 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_PEAKS_H
+# define PM_PEAKS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/** pmPeakType
+ *
+ *  A peak pixel may have several features which may be determined when the
+ *  peak is found or measured. These are specified by the pmPeakType enum.
+ *  PM_PEAK_LONE represents a single pixel which is higher than its 8 immediate
+ *  neighbors.  The PM_PEAK_EDGE represents a peak pixel which touching the image
+ *  edge. The PM_PEAK_FLAT represents a peak pixel which has more than a specific
+ *  number of neighbors at the same value, within some tolarence:
+ *
+ */
+typedef enum {
+    PM_PEAK_LONE,                       ///< Isolated peak.
+    PM_PEAK_EDGE,                       ///< Peak on edge.
+    PM_PEAK_FLAT,                       ///< Peak has equal-value neighbors.
+    PM_PEAK_UNDEF                       ///< Undefined.
+} pmPeakType;
+
+
+/** pmPeak data structure
+ *
+ *  A source has the capacity for several types of measurements. The
+ *  simplest measurement of a source is the location and flux of the peak pixel
+ *  associated with the source:
+ *
+ */
+typedef struct
+{
+    const int id;   ///< Unique ID for object
+    int x;                              ///< X-coordinate of peak pixel.
+    int y;                              ///< Y-coordinate of peak pixel.
+    float xf;                           ///< bicube fit to peak coord (x)
+    float yf;                           ///< bicube fit to peak coord (y)
+    float dx;                           ///< bicube fit error on peak coord (x)
+    float dy;                           ///< bicube fit error on peak coord (y)
+    float value;                        ///< level in detection image
+    float flux;                         ///< level in unsmoothed sci image
+    float SN;                           ///< S/N implied by detection level
+    pmPeakType type;   ///< Description of peak.
+}
+pmPeak;
+
+
+/** pmPeakAlloc()
+ *
+ *  @return pmPeak*    newly allocated pmPeak with all internal pointers set to NULL
+ */
+pmPeak *pmPeakAlloc(
+    int x,    ///< Row-coordinate in image space
+    int y,    ///< Col-coordinate in image space
+    float counts,   ///< The value of the peak pixel
+    pmPeakType type   ///< The type of peak pixel
+);
+
+bool pmPeakTest(const psPtr ptr);
+
+/** pmPeaksInVector()
+ *
+ * Find all local peaks in the given vector above the given threshold. A peak
+ * is defined as any element with a value greater than its two neighbors and with
+ * a value above the threshold. Two types of special cases must be addressed.
+ * Equal value elements: If an element has the same value as the following
+ * element, it is not considered a peak. If an element has the same value as the
+ * preceding element (but not the following), then it is considered a peak. Note
+ * that this rule (arbitrarily) identifies flat regions by their trailing edge.
+ * Edge cases: At start of the vector, the element must be higher than its
+ * neighbor. At the end of the vector, the element must be higher or equal to its
+ * neighbor. These two rules again places the peak associated with a flat region
+ * which touches the image edge at the image edge. The result of this function is
+ * a vector containing the coordinates (element number) of the detected peaks
+ * (type psU32).
+ *
+ */
+psVector *pmPeaksInVector(
+    const psVector *vector,  ///< The input vector (float)
+    float threshold   ///< Threshold above which to find a peak
+);
+
+
+/** pmPeaksInImage()
+ *
+ * Find all local peaks in the given image above the given threshold. This
+ * function should find all row peaks using pmFindVectorPeaks, then test each row
+ * peak and exclude peaks which are not local peaks. A peak is a local peak if it
+ * has a higher value than all 8 neighbors. If the peak has the same value as its
+ * +y neighbor or +x neighbor, it is NOT a local peak. If any other neighbors
+ * have an equal value, the peak is considered a valid peak. Note two points:
+ * first, the +x neighbor condition is already enforced by pmFindVectorPeaks.
+ * Second, these rules have the effect of making flat-topped regions have single
+ * peaks at the (+x,+y) corner. When selecting the peaks, their type must also be
+ * set. The result of this function is an array of pmPeak entries.
+ *
+ */
+psArray *pmPeaksInImage(
+    const psImage *image,  ///< The input image where peaks will be found (float)
+    float threshold   ///< Threshold above which to find a peak
+);
+
+
+/** pmCullPeaks()
+ *
+ * Eliminate peaks from the psList that have a peak value above the given
+ * maximum, or fall outside the valid region.
+ *
+ */
+psList *pmCullPeaks(
+    psList *peaks,   ///< The psList of peaks to be culled
+    float maxValue,   ///< Cull peaks above this value
+    const psRegion valid                ///< Cull peaks otside this psRegion
+);
+
+
+/** pmPeaksSubset()
+ *
+ * Create a new peaks array, removing certain types of peaks from the input
+ * array of peaks based on the given criteria. Peaks should be eliminated if they
+ * have a peak value above the given maximum value limit or if the fall outside
+ * the valid region.  The result of the function is a new array with a reduced
+ * number of peaks.
+ *
+ */
+psArray *pmPeaksSubset(
+    psArray *peaks,                     ///< Add comment.
+    float maxvalue,                     ///< Add comment.
+    const psRegion valid                ///< Add comment.
+);
+
+int pmPeaksCompareAscend (const void **a, const void **b);
+int pmPeaksCompareDescend (const void **a, const void **b);
+
+int pmPeakSortBySN (const void **a, const void **b);
+int pmPeakSortByY (const void **a, const void **b);
+
+/// @}
+# endif /* PM_PEAKS_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmResiduals.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmResiduals.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmResiduals.c	(revision 15358)
@@ -0,0 +1,52 @@
+/** @file pmResiduals.c
+ *
+ * Functions to manipulate the residual tables (data - model).
+ *
+ * @author EAM, IfA
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-04-21 19:47:14 $
+ * Copyright 2004 IfA, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+#include "pmResiduals.h"
+
+static void pmResidualsFree (pmResiduals *resid) {
+
+    if (resid == NULL) return;
+
+    psFree (resid->Ro);
+    psFree (resid->Rx);
+    psFree (resid->Ry);
+    psFree (resid->weight);
+    psFree (resid->mask);
+    return;
+}
+
+pmResiduals *pmResidualsAlloc (int xSize, int ySize, int xBin, int yBin) {
+
+    pmResiduals *resid = (pmResiduals *) psAlloc(sizeof(pmResiduals));
+    psMemSetDeallocator(resid, (psFreeFunc) pmResidualsFree);
+
+    int nX = xSize * xBin;
+    int nY = ySize * yBin;
+
+    nX = (nX % 2) ? nX : nX + 1;
+    nY = (nY % 2) ? nY : nY + 1;
+
+    resid->Ro  = psImageAlloc (nX, nY, PS_TYPE_F32);
+    resid->Rx  = psImageAlloc (nX, nY, PS_TYPE_F32);
+    resid->Ry  = psImageAlloc (nX, nY, PS_TYPE_F32);
+    resid->weight = psImageAlloc (nX, nY, PS_TYPE_F32);
+    resid->mask   = psImageAlloc (nX, nY, PS_TYPE_U8);
+
+    resid->xBin = xBin;
+    resid->yBin = yBin;
+    resid->xCenter = 0.5*(nX - 1); 
+    resid->yCenter = 0.5*(nY - 1);
+    return resid;
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmResiduals.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmResiduals.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmResiduals.h	(revision 15358)
@@ -0,0 +1,33 @@
+/** @file pmResiduals.h
+ *
+ * Functions to manipulate the residual tables (data - model).
+ *
+ * @author EAM, IfA
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-04-21 19:47:14 $
+ * Copyright 2004 IfA, University of Hawaii
+ */
+
+# ifndef PM_RESIDUALS_H
+# define PM_RESIDUALS_H
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/** residual tables for sources 
+ */
+typedef struct {
+    psImage *Ro;
+    psImage *Rx;
+    psImage *Ry;
+    psImage *weight;
+    psImage *mask;
+    int xBin;
+    int yBin;
+    int xCenter;
+    int yCenter;
+} pmResiduals;
+
+pmResiduals *pmResidualsAlloc (int xSize, int ySize, int xBin, int yBin);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSource.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSource.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSource.c	(revision 15358)
@@ -0,0 +1,975 @@
+/** @file  pmSource.c
+ *
+ *  Functions to define and manipulate sources on images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.40 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-10-06 02:29:47 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAMaskWeight.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+
+static void sourceFree(pmSource *tmp)
+{
+    if (!tmp)
+        return;
+
+    psTrace("psModules.objects", 5, "---- begin ----\n");
+    psFree(tmp->peak);
+    psFree(tmp->pixels);
+    psFree(tmp->weight);
+    psFree(tmp->maskObj);
+    psFree(tmp->maskView);
+    psFree(tmp->modelFlux);
+    psFree(tmp->psfFlux);
+    psFree(tmp->moments);
+    psFree(tmp->modelPSF);
+    psFree(tmp->modelEXT);
+    psFree(tmp->modelConv);
+    psFree(tmp->blends);
+    psTrace("psModules.objects", 5, "---- end ----\n");
+}
+
+// free only the pixel data associated with this source
+void pmSourceFreePixels(pmSource *source)
+{
+
+    if (!source)
+        return;
+
+    psFree (source->pixels);
+    psFree (source->weight);
+    psFree (source->maskObj);
+    psFree (source->maskView);
+    psFree (source->modelFlux);
+    psFree (source->psfFlux);
+
+    source->pixels = NULL;
+    source->weight = NULL;
+    source->maskObj = NULL;
+    source->maskView = NULL;
+    source->modelFlux = NULL;
+    source->psfFlux = NULL;
+    return;
+}
+
+/******************************************************************************
+pmSourceAlloc(): Allocate the pmSource structure and initialize its members
+to NULL.
+*****************************************************************************/
+pmSource *pmSourceAlloc()
+{
+    psTrace("psModules.objects", 5, "---- begin ----\n");
+    static int id = 1;
+    pmSource *source = (pmSource *) psAlloc(sizeof(pmSource));
+    *(int *)&source->id = id++;
+    source->seq = -1;
+    source->peak = NULL;
+    source->pixels = NULL;
+    source->weight = NULL;
+    source->maskObj = NULL;
+    source->maskView = NULL;
+    source->modelFlux = NULL;
+    source->psfFlux = NULL;
+    source->moments = NULL;
+    source->blends = NULL;
+    source->modelPSF = NULL;
+    source->modelEXT = NULL;
+    source->modelConv = NULL;
+    source->type = PM_SOURCE_TYPE_UNKNOWN;
+    source->mode = PM_SOURCE_MODE_DEFAULT;
+    psMemSetDeallocator(source, (psFreeFunc) sourceFree);
+
+    // default values are NAN
+    source->psfMag = NAN;
+    source->extMag = NAN;
+    source->errMag = NAN;
+    source->apMag  = NAN;
+    source->sky    = NAN;
+    source->skyErr = NAN;
+    source->pixWeight = NAN;
+
+    source->psfProb = NAN;
+    source->crNsigma = NAN;
+    source->extNsigma = NAN;
+
+    psTrace("psModules.objects", 5, "---- end ----\n");
+    return(source);
+}
+
+bool pmSourceTest(const psPtr ptr)
+{
+    return (psMemGetDeallocator(ptr) == (psFreeFunc)sourceFree);
+}
+
+/******************************************************************************
+pmSourceCopy(): copy the pmSource structure and contents
+XXX : are we OK with incrementing the ID?
+*****************************************************************************/
+pmSource *pmSourceCopy(pmSource *in)
+{
+    // this copy is used to allow multiple fit attempts on the
+    // same object.  the pixels, weight, and mask arrays all point to
+    // the same original subarrays.  the peak and moments point at
+    // the original values.
+    pmSource *source = pmSourceAlloc ();
+
+    // this is actually the same peak as the original, is this the correct way to handle this?
+    source->peak = pmPeakAlloc (in->peak->x, in->peak->y, in->peak->value, in->peak->type);
+    source->peak->xf = in->peak->xf;
+    source->peak->yf = in->peak->yf;
+    source->peak->flux = in->peak->flux;
+    source->peak->SN = in->peak->SN;
+
+    // copy the values in the moments structure
+    source->moments  =  pmMomentsAlloc();
+    *source->moments = *in->moments;
+
+    // These images are all views to the parent.
+    // We want a new view, but pointing at the same pixels.
+    source->pixels   = psImageCopyView (NULL, in->pixels);
+    source->weight   = psImageCopyView (NULL, in->weight);
+    source->maskView = psImageCopyView (NULL, in->maskView);
+
+    // the maskObj is a unique mask array; create a new mask image
+    if (in->maskObj) {
+        source->maskObj = psImageCopy (NULL, in->maskObj, PS_TYPE_MASK);
+    }
+
+    source->type = in->type;
+    source->mode = in->mode;
+
+    return(source);
+}
+
+// x,y are defined in the parent image coords of readout->image
+bool pmSourceDefinePixels(pmSource *mySource,
+                          const pmReadout *readout,
+                          psF32 x,
+                          psF32 y,
+                          psF32 Radius)
+{
+    psRegion srcRegion;
+
+    // Grab a subimage of the original image of size (2 * outerRadius).
+    srcRegion = psRegionForSquare (x, y, Radius);
+    srcRegion = psRegionForImage (readout->image, srcRegion);
+
+    // these images are subset images of the equivalent parents
+    mySource->pixels = psImageSubset(readout->image, srcRegion);
+    if (readout->weight) {
+        mySource->weight = psImageSubset(readout->weight, srcRegion);
+    }
+    if (readout->mask) {
+        mySource->maskView = psImageSubset(readout->mask,  srcRegion);
+        // the object mask is a copy, and used to define the source pixels
+        mySource->maskObj = psImageCopy(NULL, mySource->maskView, PS_TYPE_MASK);
+    }
+    mySource->region   = srcRegion;
+
+    return true;
+}
+
+bool pmSourceRedefinePixels(pmSource *mySource,
+                            const pmReadout *readout,
+                            psF32 x,
+                            psF32 y,
+                            psF32 Radius)
+{
+    bool extend;
+    psRegion newRegion;
+
+    if (Radius == 0)
+        return false;
+
+    // check to see if new region is completely contained within old region
+    newRegion = psRegionForSquare (x, y, Radius);
+    newRegion = psRegionForImage (readout->image, newRegion);
+
+    extend = false;
+    extend |= (int)(newRegion.x0) < (int)(mySource->region.x0);
+    extend |= (int)(newRegion.x1) > (int)(mySource->region.x1);
+    extend |= (int)(newRegion.y0) < (int)(mySource->region.y0);
+    extend |= (int)(newRegion.y1) > (int)(mySource->region.y1);
+
+    extend |= (mySource->pixels == NULL);
+    extend |= (mySource->weight == NULL);
+    extend |= (mySource->maskObj == NULL);
+    extend |= (mySource->maskView == NULL);
+
+    // extend = true;
+    if (extend) {
+        // re-create the subimage
+        psFree (mySource->pixels);
+        psFree (mySource->weight);
+        psFree (mySource->maskView);
+
+        mySource->pixels   = psImageSubset(readout->image,  newRegion);
+        mySource->weight   = psImageSubset(readout->weight, newRegion);
+        mySource->maskView = psImageSubset(readout->mask,   newRegion);
+        mySource->region   = newRegion;
+
+        // re-copy the main mask pixels.  NOTE: the user will need to reset the object mask
+        // pixels (eg, with psImageKeepCircle)
+        mySource->maskObj = psImageCopy (mySource->maskObj, mySource->maskView, PS_TYPE_MASK);
+
+        // drop the old modelFlux pixels and force the user to re-create
+        psFree (mySource->modelFlux);
+        mySource->modelFlux = NULL;
+
+        // drop the old psfFlux pixels and force the user to re-create
+        psFree (mySource->psfFlux);
+        mySource->psfFlux = NULL;
+    }
+    return extend;
+}
+
+/******************************************************************************
+    pmSourcePSFClump(source, recipe): Find the likely PSF clump in the
+    sigma-x, sigma-y plane. return 0,0 clump in case of error.
+*****************************************************************************/
+
+// XXX EAM include a S/N cutoff in selecting the sources?
+// XXX this function should probably accept the values, not a recipe. wrap with a
+// psphot-specific function which applies the recipe values
+pmPSFClump pmSourcePSFClump(psArray *sources, psMetadata *recipe)
+{
+    psTrace("psModules.objects", 5, "---- begin ----\n");
+
+    # define NPIX 10
+    # define SCALE 0.1
+
+    psArray *peaks  = NULL;
+    pmPSFClump errorClump = {-1.0, -1.0, 0.0, 0.0};
+    pmPSFClump emptyClump = {+0.0, +0.0, 0.0, 0.0};
+    pmPSFClump psfClump;
+
+    PS_ASSERT_PTR_NON_NULL(sources, errorClump);
+    PS_ASSERT_PTR_NON_NULL(recipe, errorClump);
+
+    bool status = true;
+    float PSF_CLUMP_SN_LIM = psMetadataLookupF32 (&status, recipe, "PSF_CLUMP_SN_LIM");
+    if (!status) {
+        PSF_CLUMP_SN_LIM = 0;
+    }
+
+    // find the sigmaX, sigmaY clump
+    {
+        psStats *stats  = NULL;
+        psImage *splane = NULL;
+        int binX, binY;
+        bool status;
+
+        psF32 SX_MAX = psMetadataLookupF32 (&status, recipe, "MOMENTS_SX_MAX");
+        if (!status)
+            SX_MAX = 10.0;
+        psF32 SY_MAX = psMetadataLookupF32 (&status, recipe, "MOMENTS_SY_MAX");
+        if (!status)
+            SY_MAX = 10.0;
+        psF32 AR_MAX = psMetadataLookupF32 (&status, recipe, "MOMENTS_AR_MAX");
+        if (!status)
+            AR_MAX =  3.0;
+        psF32 AR_MIN = 1.0 / AR_MAX;
+
+        // construct a sigma-plane image
+        splane = psImageAlloc (SX_MAX/SCALE, SY_MAX/SCALE, PS_TYPE_F32);
+        psImageInit(splane, 0);  // psImageAlloc doesn't zero the data
+
+        // place the sources in the sigma-plane image (ignore 0,0 values?)
+        int nValid = 0;
+        for (psS32 i = 0 ; i < sources->n ; i++)
+        {
+            pmSource *tmpSrc = (pmSource *) sources->data[i];
+            if (tmpSrc == NULL)
+                continue;
+            if (tmpSrc->moments == NULL)
+                continue;
+            if (tmpSrc->moments->SN < PSF_CLUMP_SN_LIM)
+                continue;
+
+            // Sx,Sy are limited at 0.  a peak at 0,0 is artificial
+            if (fabs(tmpSrc->moments->Sx) < 0.05)
+                continue;
+            if (fabs(tmpSrc->moments->Sy) < 0.05)
+                continue;
+            if ((tmpSrc->moments->Sx / tmpSrc->moments->Sy) > AR_MAX)
+                continue;
+            if ((tmpSrc->moments->Sx / tmpSrc->moments->Sy) < AR_MIN)
+                continue;
+            if (tmpSrc->mode & PM_SOURCE_MODE_BLEND)
+                continue;
+
+            // for the moment, force splane dimensions to be 10x10 image pix
+            binX = tmpSrc->moments->Sx/SCALE;
+            if (binX < 0)
+                continue;
+            if (binX >= splane->numCols)
+                continue;
+
+            binY = tmpSrc->moments->Sy/SCALE;
+            if (binY < 0)
+                continue;
+            if (binY >= splane->numRows)
+                continue;
+
+            splane->data.F32[binY][binX] += 1.0;
+            nValid ++;
+        }
+
+        // find the peak in this image
+        stats = psStatsAlloc (PS_STAT_MAX);
+        if (!psImageStats (stats, splane, NULL, 0)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to get image statistics.\n");
+            psFree(stats);
+            psFree(splane);
+            return emptyClump;
+        }
+        peaks = pmPeaksInImage (splane, stats[0].max / 2);
+        psTrace ("psModules.objects", 2, "clump threshold is %f\n", stats[0].max/2);
+
+        const bool keep_psf_clump = psMetadataLookupBool(NULL, recipe, "KEEP_PSF_CLUMP");
+        if (keep_psf_clump)
+        {
+            psMetadataAdd(recipe, PS_LIST_TAIL,
+                          "PSF_CLUMP", PS_DATA_IMAGE, "Image of PSF coefficients", splane);
+        }
+        psFree (splane);
+        psFree (stats);
+
+        // if we failed to find a valid peak, return the empty clump (failure signal)
+        if (peaks == NULL)
+        {
+            psLogMsg ("psphot", 3, "failed to find a peak in the PSF clump image\n");
+            if (nValid == 0) {
+                psLogMsg ("psphot", 3, "no valid sources kept for PSF search\n");
+            } else {
+                psLogMsg ("psphot", 3, "no significant peak\n");
+            }
+            return (emptyClump);
+        }
+    }
+
+    // measure statistics on Sx, Sy if Sx, Sy within range of clump
+    {
+        pmPeak *clump;
+        psF32 minSx, maxSx;
+        psF32 minSy, maxSy;
+        psVector *tmpSx = NULL;
+        psVector *tmpSy = NULL;
+        psStats *stats  = NULL;
+
+        // select the single highest peak
+        psArraySort (peaks, pmPeaksCompareDescend);
+        clump = peaks->data[0];
+        psTrace ("psModules.objects", 2, "clump is at %d, %d (%f)\n", clump->x, clump->y, clump->value);
+
+        // define section window for clump
+        minSx = clump->x * SCALE - 0.2;
+        maxSx = clump->x * SCALE + 0.2;
+        minSy = clump->y * SCALE - 0.2;
+        maxSy = clump->y * SCALE + 0.2;
+
+        tmpSx = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+        tmpSy = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+
+        // create vectors with Sx, Sy values in window
+        // clip sources based on S/N
+        for (psS32 i = 0 ; i < sources->n ; i++)
+        {
+            pmSource *tmpSrc = (pmSource *) sources->data[i];
+
+            if (tmpSrc == NULL)
+                continue;
+            if (tmpSrc->moments == NULL)
+                continue;
+            if (tmpSrc->moments->SN < PSF_CLUMP_SN_LIM)
+                continue;
+
+            if (tmpSrc->moments->Sx < minSx)
+                continue;
+            if (tmpSrc->moments->Sx > maxSx)
+                continue;
+            if (tmpSrc->moments->Sy < minSy)
+                continue;
+            if (tmpSrc->moments->Sy > maxSy)
+                continue;
+            tmpSx->data.F32[tmpSx->n] = tmpSrc->moments->Sx;
+            tmpSy->data.F32[tmpSy->n] = tmpSrc->moments->Sy;
+            tmpSx->n++;
+            tmpSy->n++;
+            if (tmpSx->n == tmpSx->nalloc) {
+                psVectorRealloc (tmpSx, tmpSx->nalloc + 100);
+                psVectorRealloc (tmpSy, tmpSy->nalloc + 100);
+            }
+        }
+
+        // measures stats of Sx, Sy
+        stats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+
+        psVectorStats (stats, tmpSx, NULL, NULL, 0);
+        psfClump.X  = stats->clippedMean;
+        psfClump.dX = stats->clippedStdev;
+
+        psVectorStats (stats, tmpSy, NULL, NULL, 0);
+        psfClump.Y  = stats->clippedMean;
+        psfClump.dY = stats->clippedStdev;
+
+        psTrace ("psModules.objects", 2, "clump  X,  Y: %f, %f\n", psfClump.X, psfClump.Y);
+        psTrace ("psModules.objects", 2, "clump DX, DY: %f, %f\n", psfClump.dX, psfClump.dY);
+
+        psFree (stats);
+        psFree (peaks);
+        psFree (tmpSx);
+        psFree (tmpSy);
+    }
+
+    psTrace("psModules.objects", 5, "---- end ----\n");
+    return (psfClump);
+}
+
+/******************************************************************************
+    pmSourceRoughClass(source, recipe): make a guess at the source
+    classification.
+    XXX: How can this function ever return FALSE?
+*****************************************************************************/
+
+bool pmSourceRoughClass(psArray *sources, psMetadata *recipe, pmPSFClump clump, psMaskType maskSat)
+{
+    psTrace("psModules.objects", 5, "---- begin ----");
+
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(recipe, false);
+
+    int Nsat     = 0;
+    int Next     = 0;
+    int Nstar    = 0;
+    int Npsf     = 0;
+    int Ncr      = 0;
+    int Nsatstar = 0;
+    psRegion inner;
+
+    // report stats on S/N values for star-like objects
+    psVector *starsn_peaks = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *starsn_moments = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+
+    // get basic parameters, or set defaults
+    bool status;
+    float PSF_SN_LIM = psMetadataLookupF32 (&status, recipe, "PSF_SN_LIM");
+    if (!status)
+        PSF_SN_LIM = 20.0;
+    float PSF_CLUMP_NSIGMA = psMetadataLookupF32 (&status, recipe, "PSF_CLUMP_NSIGMA");
+    if (!status)
+        PSF_CLUMP_NSIGMA = 1.5;
+    float INNER_RADIUS = psMetadataLookupF32 (&status, recipe, "SKY_INNER_RADIUS");
+
+    // XXX allow clump size to be scaled relative to sigmas?
+    // make rough IDs based on clumpX,Y,DX,DY
+    for (psS32 i = 0 ; i < sources->n ; i++) {
+
+        pmSource *source = (pmSource *) sources->data[i];
+
+        source->peak->type = 0;
+
+        // we are basically classifying by moments; use the default if not found
+        if (!source->moments) {
+            source->type = PM_SOURCE_TYPE_STAR;
+            source->mode |= PM_SOURCE_MODE_DEFAULT;
+            Nstar++;
+            continue;
+        }
+
+        psF32 sigX = source->moments->Sx;
+        psF32 sigY = source->moments->Sy;
+
+        // XXX EAM : can we use the value of SATURATE if mask is NULL?
+        inner = psRegionForSquare (source->peak->x, source->peak->y, 2);
+        inner = psRegionForImage (source->maskView, inner);
+        int Nsatpix = psImageCountPixelMask (source->maskView, inner, maskSat);
+
+        // saturated star (size consistent with PSF or larger)
+        // Nsigma should be user-configured parameter
+        bool big = (sigX > (clump.X - clump.dX)) && (sigY > (clump.Y - clump.dY));
+        big = true;
+        if ((Nsatpix > 1) && big) {
+            source->type = PM_SOURCE_TYPE_STAR;
+            source->mode |= PM_SOURCE_MODE_SATSTAR;
+            // recalculate moments here with larger box?
+            pmSourceMoments (source, INNER_RADIUS);
+            Nsatstar ++;
+            continue;
+        }
+
+        // saturated object (not a star, eg bleed trails, hot pixels)
+        if (Nsatpix > 1) {
+            source->type = PM_SOURCE_TYPE_SATURATED;
+            source->mode |= PM_SOURCE_MODE_DEFAULT;
+            Nsat ++;
+            continue;
+        }
+
+        // likely defect (too small to be stellar) (push out to 3 sigma)
+        // low S/N objects which are small are probably stellar
+        // only set candidate defects if
+        // XXX these limits are quite arbitrary
+        if ((sigX < 0.05) || (sigY < 0.05)) {
+            source->type = PM_SOURCE_TYPE_DEFECT;
+            source->mode |= PM_SOURCE_MODE_DEFAULT;
+            Ncr ++;
+            continue;
+        }
+
+        // likely unsaturated extended source (too large to be stellar)
+        if ((sigX > (clump.X + 3*clump.dX)) || (sigY > (clump.Y + 3*clump.dY))) {
+            source->type = PM_SOURCE_TYPE_EXTENDED;
+            source->mode |= PM_SOURCE_MODE_DEFAULT;
+            Next ++;
+            continue;
+        }
+
+        // the rest are probable stellar objects
+        starsn_moments->data.F32[starsn_moments->n] = source->moments->SN;
+        starsn_moments->n ++;
+        starsn_peaks->data.F32[starsn_peaks->n] = source->peak->SN;
+        starsn_peaks->n ++;
+        Nstar ++;
+
+        // PSF star (within 1.5 sigma of clump center, S/N > limit)
+        psF32 radius = hypot ((sigX-clump.X)/clump.dX, (sigY-clump.Y)/clump.dY);
+        if ((source->moments->SN > PSF_SN_LIM) && (radius < PSF_CLUMP_NSIGMA)) {
+            source->type = PM_SOURCE_TYPE_STAR;
+            source->mode |= PM_SOURCE_MODE_PSFSTAR;
+            Npsf ++;
+            continue;
+        }
+
+        // random type of star
+        source->type = PM_SOURCE_TYPE_STAR;
+        source->mode |= PM_SOURCE_MODE_DEFAULT;
+    }
+
+    psStats *stats = NULL;
+    stats = psStatsAlloc (PS_STAT_MIN | PS_STAT_MAX);
+    if (!psVectorStats (stats, starsn_moments, NULL, NULL, 0)) {
+        // Don't care about this error
+        psErrorClear();
+    }
+    psLogMsg ("pmObjects", 3, "SN range (moments): %f - %f\n", stats[0].min, stats[0].max);
+    psFree (starsn_moments);
+    psFree (stats);
+
+    stats = psStatsAlloc (PS_STAT_MIN | PS_STAT_MAX);
+    if (!psVectorStats (stats, starsn_peaks, NULL, NULL, 0)) {
+        // Don't care about this error
+        psErrorClear();
+    }
+    psLogMsg ("pmObjects", 3, "SN range (peaks)  : %f - %f\n", stats[0].min, stats[0].max);
+    psFree (starsn_peaks);
+    psFree (stats);
+
+    psTrace ("psModules.objects", 2, "Nstar:    %3d\n", Nstar);
+    psTrace ("psModules.objects", 2, "Npsf:     %3d\n", Npsf);
+    psTrace ("psModules.objects", 2, "Next:     %3d\n", Next);
+    psTrace ("psModules.objects", 2, "Nsatstar: %3d\n", Nsatstar);
+    psTrace ("psModules.objects", 2, "Nsat:     %3d\n", Nsat);
+    psTrace ("psModules.objects", 2, "Ncr:      %3d\n", Ncr);
+
+    psTrace("psModules.objects", 5, "---- end ----\n");
+    return true;
+}
+
+/******************************************************************************
+pmSourceMoments(source, radius): this function takes a subImage defined in the
+pmSource data structure, along with the peak location, and determines the
+various moments associated with that peak.
+
+Requires the following to have been created:
+    pmSource
+    pmSource->peak
+    pmSource->pixels
+    pmSource->weight
+    pmSource->mask
+
+XXX: The peak calculations are done in image coords, not subImage coords.
+
+XXX EAM : this version clips input pixels on S/N
+XXX EAM : this version returns false for several reasons
+*****************************************************************************/
+# define VALID_RADIUS(X,Y,RAD2) (((RAD2) >= (PS_SQR(X) + PS_SQR(Y))) ? 1 : 0)
+
+bool pmSourceMoments(pmSource *source,
+                     psF32 radius)
+{
+    psTrace("psModules.objects", 5, "---- begin ----\n");
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_PTR_NON_NULL(source->pixels, false);
+    PS_ASSERT_PTR_NON_NULL(source->maskObj, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(radius, 0.0, false);
+
+    //
+    // XXX: Verify the setting for sky if source->moments == NULL.
+    //
+    psF32 sky = 0.0;
+    if (source->moments == NULL) {
+        source->moments = pmMomentsAlloc();
+    } else {
+        sky = source->moments->Sky;
+    }
+
+    //
+    // Sum = SUM (z - sky)
+    // X1  = SUM (x - xc)*(z - sky)
+    // X2  = SUM (x - xc)^2 * (z - sky)
+    // XY  = SUM (x - xc)*(y - yc)*(z - sky)
+    //
+    psF32 peakPixel = -PS_MAX_F32;
+    psS32 numPixels = 0;
+    psF32 Sum = 0.0;
+    psF32 Var = 0.0;
+    psF32 X1 = 0.0;
+    psF32 Y1 = 0.0;
+    psF32 X2 = 0.0;
+    psF32 Y2 = 0.0;
+    psF32 XY = 0.0;
+    psF32 x  = 0;
+    psF32 y  = 0;
+    psF32 R2 = PS_SQR(radius);
+
+    psF32 xPeak = source->peak->x;
+    psF32 yPeak = source->peak->y;
+    psF32 xOff = source->pixels->col0 - source->peak->x;
+    psF32 yOff = source->pixels->row0 - source->peak->y;
+
+    // XXX why do I get different results for these two methods of finding Sx?
+    // XXX Sx, Sy would be better measured if we clip pixels close to sky
+    // XXX Sx, Sy can still be imaginary, so we probably need to keep Sx^2?
+    // We loop through all pixels in this subimage (source->pixels), and for each
+    // pixel that is not masked, AND within the radius of the peak pixel, we
+    // proceed with the moments calculation.  need to do two loops for a
+    // numerically stable result.  first loop: get the sums.
+    // XXX EAM : mask == 0 is valid
+
+    for (psS32 row = 0; row < source->pixels->numRows ; row++) {
+
+        psF32 *vPix = source->pixels->data.F32[row];
+        psF32 *vWgt = source->weight->data.F32[row];
+        psU8  *vMsk = (source->maskObj == NULL) ? NULL : source->maskObj->data.U8[row];
+
+        for (psS32 col = 0; col < source->pixels->numCols ; col++, vPix++, vWgt++) {
+            if ((vMsk != NULL) && *vMsk) {
+                vMsk++;
+                continue;
+            }
+
+            psF32 xDiff = col + xOff;
+            psF32 yDiff = row + yOff;
+
+            // radius is just a function of (xDiff, yDiff)
+            if (!VALID_RADIUS(xDiff, yDiff, R2)) {
+                if (vMsk != NULL)
+                    vMsk++;
+                continue;
+            }
+
+            psF32 pDiff = *vPix - sky;
+            psF32 wDiff = *vWgt;
+
+            // XXX EAM : check for valid S/N in pixel
+            // XXX EAM : should this limit be user-defined?
+            if (PS_SQR(pDiff) < wDiff) {
+                if (vMsk != NULL)
+                    vMsk++;
+                continue;
+            }
+
+            Var += wDiff;
+            Sum += pDiff;
+
+            psF32 xWght = xDiff * pDiff;
+            psF32 yWght = yDiff * pDiff;
+
+            X1  += xWght;
+            Y1  += yWght;
+
+            XY  += xDiff * yWght;
+            X2  += xDiff * xWght;
+            Y2  += yDiff * yWght;
+
+            peakPixel = PS_MAX (*vPix, peakPixel);
+            numPixels++;
+            if (vMsk != NULL)
+                vMsk++;
+        }
+    }
+
+    // if we have less than (1/4) of the possible pixels, force a retry
+    // XXX EAM - the limit is a bit arbitrary.  make it user defined?
+    if ((numPixels < 0.75*R2) || (Sum <= 0)) {
+        psTrace ("psModules.objects", 3, "no valid pixels for source\n");
+        psTrace("psModules.objects", 5, "---- end (false) ----\n");
+        return (false);
+    }
+
+    psTrace ("psModules.objects", 4, "sky: %f  Sum: %f  X1: %f  Y1: %f  X2: %f  Y2: %f  XY: %f  Npix: %d\n",
+             sky, Sum, X1, Y1, X2, Y2, XY, numPixels);
+
+    //
+    // first moment X  = X1/Sum + xc
+    // second moment X = sqrt (X2/Sum - (X1/Sum)^2)
+    // Sxy             = XY / Sum
+    //
+    x = X1/Sum;
+    y = Y1/Sum;
+    if ((fabs(x) > radius) || (fabs(y) > radius)) {
+        psTrace ("psModules.objects", 3, "large centroid swing; invalid peak %d, %d\n",
+                 source->peak->x, source->peak->y);
+        psTrace("psModules.objects", 5, "---- end(false)  ----\n");
+        return (false);
+    }
+
+    source->moments->x = x + xPeak;
+    source->moments->y = y + yPeak;
+
+    // XXX EAM : Sxy needs to have x*y subtracted
+    source->moments->Sxy = XY/Sum - x*y;
+    source->moments->Sum = Sum;
+    source->moments->SN  = Sum / sqrt(Var);
+    source->moments->Peak = peakPixel;
+    source->moments->nPixels = numPixels;
+
+    // XXX EAM : these values can be negative, so we need to limit the range
+    // XXX EAM : make the use of this consistent: should this be the second moment or sqrt?
+    // source->moments->Sx = sqrt(PS_MAX(X2/Sum - PS_SQR(x), 0));
+    // source->moments->Sy = sqrt(PS_MAX(Y2/Sum - PS_SQR(y), 0));
+    source->moments->Sx = PS_MAX(X2/Sum - PS_SQR(x), 0);
+    source->moments->Sy = PS_MAX(Y2/Sum - PS_SQR(y), 0);
+
+    psTrace ("psModules.objects", 4,
+             "sky: %f  Sum: %f  x: %f  y: %f  Sx: %f  Sy: %f  Sxy: %f\n",
+             sky, Sum, source->moments->x, source->moments->y,
+             source->moments->Sx, source->moments->Sy, source->moments->Sxy);
+
+    psTrace("psModules.objects", 5, "---- end ----\n");
+    return(true);
+}
+
+// construct a realization of the source model
+bool pmSourceCacheModel (pmSource *source, psMaskType maskVal) {
+
+    // select appropriate model
+    pmModel *model = pmSourceGetModel (NULL, source);
+    if (model == NULL) return false;  // model must be defined
+
+    // if we already have a cached image, re-use that memory
+    source->modelFlux = psImageCopy (source->modelFlux, source->pixels, PS_TYPE_F32);
+    psImageInit (source->modelFlux, 0.0);
+
+    // in some places (psphotEnsemble), we need a normalized version
+    // in others, we just want the model.  which is more commonly used?
+    // modelFlux always has unity normalization (I0 = 1.0)
+    pmModelAdd (source->modelFlux, source->maskObj, model, PM_MODEL_OP_FULL | PM_MODEL_OP_NORM, maskVal);
+    return true;
+}
+
+// construct a realization of the source model
+// XXX this function should optionally save an existing psf image from modelFlux
+bool pmSourceCachePSF (pmSource *source, psMaskType maskVal) {
+
+    // select appropriate model
+    if (source->modelPSF == NULL) return false;  // model must be defined
+
+    // if we already have a cached image, re-use that memory
+    source->psfFlux = psImageCopy (source->psfFlux, source->pixels, PS_TYPE_F32);
+    psImageInit (source->psfFlux, 0.0);
+
+    // in some places (psphotEnsemble), we need a normalized version
+    // in others, we just want the model.  which is more commonly used?
+    // psfFlux always has unity normalization (I0 = 1.0)
+    pmModelAdd (source->psfFlux, source->maskObj, source->modelPSF, PM_MODEL_OP_FULL | PM_MODEL_OP_NORM, maskVal);
+    return true;
+}
+
+// should we call pmSourceCacheModel if it does not exist?
+bool pmSourceOp (pmSource *source, pmModelOpMode mode, bool add, psMaskType maskVal, int dx, int dy) {
+
+    bool status;
+
+    if (add) {
+        psTrace ("psphot", 3, "replacing object at %f,%f\n", source->peak->xf, source->peak->yf);
+    } else {
+        psTrace ("psphot", 3, "removing object at %f,%f\n", source->peak->xf, source->peak->yf);
+    }
+
+    pmModel *model = pmSourceGetModel (NULL, source);
+    if (model == NULL) return false;  // model must be defined
+
+    if (source->modelFlux) {
+        // add in the pixels from the modelFlux image
+        int dX = source->modelFlux->col0 - source->pixels->col0;
+        int dY = source->modelFlux->row0 - source->pixels->row0;
+        assert (dX >= 0);
+        assert (dY >= 0);
+        assert (dX + source->modelFlux->numCols <= source->pixels->numCols);
+        assert (dY + source->modelFlux->numRows <= source->pixels->numRows);
+
+        // modelFlux has unity normalization
+        float Io = model->params->data.F32[PM_PAR_I0];
+        if (mode & PM_MODEL_OP_NORM) {
+            Io = 1.0;
+        }
+
+        psU8 **mask = NULL;
+        if (source->maskObj) {
+            mask = source->maskObj->data.U8;
+        }
+
+        psF32 **target = source->pixels->data.F32;
+        if (mode & PM_MODEL_OP_NOISE) {
+            target = source->weight->data.F32;
+        }
+
+        // XXX need to respect the source and model masks?
+        for (int iy = 0; iy < source->modelFlux->numRows; iy++) {
+            int oy = iy + dY;
+            for (int ix = 0; ix < source->modelFlux->numCols; ix++) {
+                int ox = ix + dX;
+                if (mask && (mask[iy][ix] & maskVal)) continue;
+                float value = Io*source->modelFlux->data.F32[iy][ix];
+                if (add) {
+                    target[oy][ox] += value;
+                } else {
+                    target[oy][ox] -= value;
+                }
+            }
+        }
+        return true;
+    }
+
+    psImage *target = source->pixels;
+    if (mode & PM_MODEL_OP_NOISE) {
+        target = source->weight;
+    }
+
+    if (add) {
+        status = pmModelAddWithOffset (target, source->maskObj, model, PM_MODEL_OP_FULL, maskVal, dx, dy);
+    } else {
+        status = pmModelSubWithOffset (target, source->maskObj, model, PM_MODEL_OP_FULL, maskVal, dx, dy);
+    }
+
+    return true;
+}
+
+bool pmSourceAdd (pmSource *source, pmModelOpMode mode, psMaskType maskVal) {
+    return pmSourceOp (source, mode, true, maskVal, 0, 0);
+}
+
+bool pmSourceSub (pmSource *source, pmModelOpMode mode, psMaskType maskVal) {
+    return pmSourceOp (source, mode, false, maskVal, 0, 0);
+}
+
+bool pmSourceAddWithOffset (pmSource *source, pmModelOpMode mode, psMaskType maskVal, int dx, int dy) {
+    return pmSourceOp (source, mode, true, maskVal, dx, dy);
+}
+
+bool pmSourceSubWithOffset (pmSource *source, pmModelOpMode mode, psMaskType maskVal, int dx, int dy) {
+    return pmSourceOp (source, mode, false, maskVal, dx, dy);
+}
+
+// given a source, which model is currently appropriate?
+// choose PSF or EXT based on source->type, but fall back on PSF
+// if the EXT model is NULL
+pmModel *pmSourceGetModel (bool *isPSF, const pmSource *source)
+{
+
+    pmModel *model;
+
+    if (isPSF) {
+        *isPSF = false;
+    }
+
+    switch (source->type) {
+      case PM_SOURCE_TYPE_STAR:
+        model = source->modelPSF;
+        if (model == NULL)
+            return NULL;
+        if (isPSF) {
+            *isPSF = true;
+        }
+        return model;
+
+// XXX when should I return the modelConv ??
+      case PM_SOURCE_TYPE_EXTENDED:
+        model = source->modelConv;
+        if (!model) {
+            model = source->modelEXT;
+        }
+        if (!model && source->modelPSF) {
+            if (isPSF) {
+                *isPSF = true;
+            }
+            return source->modelPSF;
+        }
+        return (model);
+        break;
+
+      default:
+        return NULL;
+    }
+    return NULL;
+}
+
+// sort by SN (descending)
+int pmSourceSortBySN (const void **a, const void **b)
+{
+    pmSource *A = *(pmSource **)a;
+    pmSource *B = *(pmSource **)b;
+
+    psF32 fA = (A->peak == NULL) ? 0 : A->peak->SN;
+    psF32 fB = (B->peak == NULL) ? 0 : B->peak->SN;
+    if (isnan (fA)) fA = 0;
+    if (isnan (fB)) fB = 0;
+
+    psF32 diff = fA - fB;
+    if (diff > FLT_EPSILON) return (-1);
+    if (diff < FLT_EPSILON) return (+1);
+    return (0);
+}
+
+// sort by Y (ascending)
+int pmSourceSortByY (const void **a, const void **b)
+{
+    pmSource *A = *(pmSource **)a;
+    pmSource *B = *(pmSource **)b;
+
+    psF32 fA = (A->peak == NULL) ? 0 : A->peak->y;
+    psF32 fB = (B->peak == NULL) ? 0 : B->peak->y;
+
+    psF32 diff = fA - fB;
+    if (diff > FLT_EPSILON) return (+1);
+    if (diff < FLT_EPSILON) return (-1);
+    return (0);
+}
+
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSource.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSource.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSource.h	(revision 15358)
@@ -0,0 +1,233 @@
+/* @file  pmSource.h
+ *
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.18 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-09-27 03:35:29 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_SOURCE_H
+# define PM_SOURCE_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/** pmSourceType enumeration
+ *
+ * A given source may be identified as most-likely to be one of several source
+ * types. The pmSource entry pmSourceType defines the current best-guess for this
+ * source.
+ *
+ * XXX: The values given below are currently illustrative and will require
+ * some modification as the source classification code is developed. (TBD)
+ *
+ */
+typedef enum {
+    PM_SOURCE_TYPE_UNKNOWN,  ///< a cosmic-ray
+    PM_SOURCE_TYPE_DEFECT,  ///< a cosmic-ray
+    PM_SOURCE_TYPE_SATURATED,  ///< random saturated pixels
+    PM_SOURCE_TYPE_STAR,  ///< a good-quality star
+    PM_SOURCE_TYPE_EXTENDED,  ///< an extended object (eg, galaxy)
+} pmSourceType;
+
+typedef enum {
+    PM_SOURCE_MODE_DEFAULT    = 0x0000, ///<
+    PM_SOURCE_MODE_PSFMODEL   = 0x0001, ///<
+    PM_SOURCE_MODE_EXTMODEL   = 0x0002, ///<
+    PM_SOURCE_MODE_SUBTRACTED = 0x0004, ///<
+    PM_SOURCE_MODE_FITTED     = 0x0008, ///<
+    PM_SOURCE_MODE_FAIL       = 0x0010, ///<
+    PM_SOURCE_MODE_POOR       = 0x0020, ///<
+    PM_SOURCE_MODE_PAIR       = 0x0040, ///<
+    PM_SOURCE_MODE_PSFSTAR    = 0x0080, ///<
+    PM_SOURCE_MODE_SATSTAR    = 0x0100, ///<
+    PM_SOURCE_MODE_BLEND      = 0x0200, ///<
+    PM_SOURCE_MODE_LINEAR     = 0x0400, ///<
+    PM_SOURCE_MODE_TEMPSUB    = 0x0800, ///< XXX get me a better name!
+    PM_SOURCE_MODE_EXTERNAL   = 0x1000, ///< XXX get me a better name!
+    PM_SOURCE_MODE_BADPSF     = 0x2000, ///< Failed to get good estimate of object's PSF
+} pmSourceMode;
+
+/** pmSource data structure
+ *
+ *  This source has the capacity for several types of measurements. The
+ *  simplest measurement of a source is the location and flux of the peak pixel
+ *  associated with the source:
+ *
+ */
+struct pmSource {
+    const int id;                       ///< Unique ID for object
+    int seq;				///< ID for output (generated on write)
+    pmPeak *peak;                       ///< Description of peak pixel.
+    psImage *pixels;                    ///< Rectangular region including object pixels.
+    psImage *weight;                    ///< Image variance.
+    psImage *maskObj;                   ///< unique mask for this object which marks included pixels associated with objects.
+    psImage *maskView;                  ///< view into global image mask for this object region
+    psImage *modelFlux;                 ///< cached copy of the best model for this source
+    psImage *psfFlux;                   ///< cached copy of the psf model for this source
+    pmMoments *moments;                 ///< Basic moments measure for the object.
+    pmModel *modelPSF;                  ///< PSF Model fit (parameters and type)
+    pmModel *modelEXT;                  ///< EXT (floating) Model fit (parameters and type).
+    pmModel *modelConv;                 ///< PSF-Convolved Model fit (parameters and type).
+    pmSourceType type;                  ///< Best identification of object.
+    pmSourceMode mode;                  ///< Best identification of object.
+    psArray *blends;
+    float psfMag;                       ///< calculated from flux in modelPsf
+    float extMag;                       ///< calculated from flux in modelEXT
+    float errMag;                       ///< error in psfMag OR extMag (depending on type)
+    float apMag;                        ///< apMag corresponding to psfMag or extMag (depending on type)
+    float pixWeight;                    ///< model-weighted coverage of valid pixels
+    float psfProb;                      ///< probability of PSF
+    float crNsigma;                     ///< Nsigma deviation from PSF to CR
+    float extNsigma;                    ///< Nsigma deviation from PSF to EXT
+    psRegion region;                    ///< area on image covered by selected pixels
+    float sky, skyErr;                  ///< The sky and its error at the center of the object
+};
+
+/** pmPSFClump data structure
+ *
+ * A collection of object moment measurements can be used to determine
+ * approximate object classes. The key to this analysis is the location and
+ * statistics (in the second-moment plane,
+ *
+ */
+typedef struct
+{
+    float X;
+    float dX;
+    float Y;
+    float dY;
+}
+pmPSFClump;
+
+
+/** pmSourceAlloc()
+ *
+ */
+pmSource  *pmSourceAlloc();
+
+/** pmSourceCopy()
+ *
+ */
+pmSource  *pmSourceCopy(pmSource *source);
+
+// free just the pixels for a source, keeping derived data
+void pmSourceFreePixels(pmSource *source);
+
+bool pmSourceTest(const psPtr ptr);
+
+/** pmSourceDefinePixels()
+ *
+ * Define psImage subarrays for the source located at coordinates x,y on the
+ * image set defined by readout. The pixels defined by this operation consist of
+ * a square window (of full width 2Radius+1) centered on the pixel which contains
+ * the given coordinate, in the frame of the readout. The window is defined to
+ * have limits which are valid within the boundary of the readout image, thus if
+ * the radius would fall outside the image pixels, the subimage is truncated to
+ * only consist of valid pixels. If readout->mask or readout->weight are not
+ * NULL, matching subimages are defined for those images as well. This function
+ * fails if no valid pixels can be defined (x or y less than Radius, for
+ * example). This function should be used to define a region of interest around a
+ * source, including both source and sky pixels.
+ *
+ */
+bool pmSourceDefinePixels(
+    pmSource *mySource,                 ///< source to be re-defined
+    const pmReadout *readout,  ///< base the source on this readout
+    psF32 x,                            ///< center coords of source
+    psF32 y,                            ///< center coords of source
+    psF32 Radius                        ///< size of box on source
+);
+
+bool pmSourceRedefinePixels (
+    pmSource *mySource,   ///< source to be re-defined
+    const pmReadout *readout,   ///< base the source on this readout
+    psF32 x,     ///< center coords of source
+    psF32 y,      ///< center coords of source
+    psF32 Radius   ///< size of box on source
+);
+
+/** pmSourcePSFClump()
+ *
+ * We use the source moments to make an initial, approximate source
+ * classification, and as part of the information needed to build a PSF model for
+ * the image. As long as the PSF shape does not vary excessively across the
+ * image, the sources which are represented by a PSF (the start) will have very
+ * similar second moments. The function pmSourcePSFClump searches a collection of
+ * sources with measured moments for a group with moments which are all very
+ * similar. The function returns a pmPSFClump structure, representing the
+ * centroid and size of the clump in the sigma_x, sigma_y second-moment plane.
+ *
+ * The goal is to identify and characterize the stellar clump within the
+ * sigma_x, sigma_y second-moment plane.  To do this, an image is constructed to
+ * represent this plane.  The units of sigma_x and sigma_y are in image pixels. A
+ * pixel in this analysis image represents 0.1 pixels in the input image. The
+ * dimensions of the image need only be 10 pixels. The peak pixel in this image
+ * (above a threshold of half of the image maximum) is found. The coordinates of
+ * this peak pixel represent the 2D mode of the sigma_x, sigma_y distribution.
+ * The sources with sigma_x, sigma_y within 0.2 pixels of this value are then
+ *  * used to calculate the median and standard deviation of the sigma_x, sigma_y
+ * values. These resulting values are returned via the pmPSFClump structure.
+ *
+ * The return value indicates the success (TRUE) of the operation.
+ *
+ * XXX: Limit the S/N of the candidate sources (part of Metadata)? (TBD).
+ * XXX: Save the clump parameters on the Metadata (TBD)
+ *
+ */
+pmPSFClump pmSourcePSFClump(
+    psArray *source,   ///< The input pmSource
+    psMetadata *metadata  ///< Contains classification parameters
+);
+
+
+/** pmSourceRoughClass()
+ *
+ * Based on the specified data values, make a guess at the source
+ * classification. The sources are provides as a psArray of pmSource entries.
+ * Definable parameters needed to make the classification are provided to the
+ * routine with the psMetadata structure. The rules (in SDRS) refer to values which
+ * can be extracted from the metadata using the given keywords. Except as noted,
+ * the data type for these parameters are psF32.
+ *
+ */
+bool pmSourceRoughClass(
+    psArray *source,   ///< The input pmSource
+    psMetadata *metadata,  ///< Contains classification parameters
+    pmPSFClump clump,   ///< Statistics about the PSF clump
+    psMaskType maskSat                  ///< Mask value for saturated pixels
+);
+
+
+/** pmSourceMoments()
+ *
+ * Measure source moments for the given source, using the value of
+ * source.moments.sky provided as the local background value and the peak
+ * coordinates as the initial source location. The resulting moment values are
+ * applied to the source.moments entry, and the source is returned. The moments
+ * are measured within the given circular radius of the source.peak coordinates.
+ * The return value indicates the success (TRUE) of the operation.
+ *
+ */
+bool pmSourceMoments(
+    pmSource *source,   ///< The input pmSource for which moments will be computed
+    float radius   ///< Use a circle of pixels around the peak
+);
+
+pmModel *pmSourceGetModel (bool *isPSF, const pmSource *source);
+
+bool pmSourceAdd (pmSource *source, pmModelOpMode mode, psMaskType maskVal);
+bool pmSourceSub (pmSource *source, pmModelOpMode mode, psMaskType maskVal);
+bool pmSourceAddWithOffset (pmSource *source, pmModelOpMode mode, psMaskType maskVal, int dx, int dy);
+bool pmSourceSubWithOffset (pmSource *source, pmModelOpMode mode, psMaskType maskVal, int dx, int dy);
+
+bool pmSourceOp (pmSource *source, pmModelOpMode mode, bool add, psMaskType maskVal, int dx, int dy);
+bool pmSourceCacheModel (pmSource *source, psMaskType maskVal);
+bool pmSourceCachePSF (pmSource *source, psMaskType maskVal);
+
+int             pmSourceSortBySN (const void **a, const void **b);
+int             pmSourceSortByY (const void **a, const void **b);
+
+/// @}
+# endif /* PM_SOURCE_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceContour.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceContour.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceContour.c	(revision 15358)
@@ -0,0 +1,456 @@
+/** @file  pmSourceContour.c
+ *
+ *  Functions to measure the local sky and sky variance for sources on images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmSourceContour.h"
+
+/******************************************************************************
+findValue(source, level, row, col, dir): a private function which determines
+the column coordinate of the model function which has the value "level".  If
+dir equals 0, then you loop leftwards from the peak pixel, otherwise,
+rightwards.
+ 
+XXX: reverse order of row,col args?
+ 
+XXX: Input row/col are in image coords.
+ 
+XXX: The result is returned in image coords.
+*****************************************************************************/
+# define LEFT false
+# define RIGHT true
+
+// return the first coordinate at or below the threshold in the requested direction
+static int findContourNeg(
+    psImage *image,
+    float threshold,
+    int x,
+    int y,
+    bool right)
+{
+
+    psTrace("psModules.objects", 4, "---- %s() begin ----\n", __func__);
+
+    // We define variables incr and lastColumn so that we can use the same loop
+    // whether we are stepping leftwards, or rightwards.
+
+    int incr;
+    int subCol;
+    int lastColumn;
+    if (right) {
+        incr = 1;
+        lastColumn = image->numCols - 1;
+    } else {
+        incr = -1;
+        lastColumn = 0;
+    }
+
+    subCol = x;
+
+    while (subCol != lastColumn) {
+        float value = image->data.F32[y][subCol];
+        if (value <= threshold) {
+            psTrace("psModules.objects", 4, "---- %s() end ----\n", __func__);
+            return (subCol);
+        }
+        subCol += incr;
+    }
+    psTrace("psModules.objects", 4, "---- %s() end ----\n", __func__);
+    return (lastColumn);
+}
+
+// return the last coordinate at or below the threshold in the requested direction
+static int findContourPos(
+    psImage *image,
+    float threshold,
+    int x,
+    int y,
+    bool right,
+    int xEnd)
+{
+
+    psTrace("psModules.objects", 4, "---- %s() begin ----\n", __func__);
+
+    // We define variables incr and lastColumn so that we can use the same loop
+    // whether we are stepping leftwards, or rightwards.
+
+    int incr;
+    int subCol;
+    int lastColumn;
+    if (right) {
+        incr = 1;
+        lastColumn = PS_MIN (image->numCols - 1, xEnd);
+    } else {
+        incr = -1;
+        lastColumn = PS_MAX (0, xEnd);
+    }
+
+    subCol = x;
+    while (subCol != lastColumn) {
+        float value = image->data.F32[y][subCol];
+        if (value >= threshold) {
+            psTrace("psModules.objects", 4, "---- %s() end ----\n", __func__);
+            if (subCol == x) {
+                return (subCol);
+            }
+            return (subCol);
+        }
+        subCol += incr;
+    }
+    psTrace("psModules.objects", 4, "---- %s() end ----\n", __func__);
+    return (lastColumn);
+}
+
+/******************************************************************************
+findValue(source, level, row, col, dir): a private function which determines
+the column coordinate of the model function which has the value "level".  If
+dir equals 0, then you loop leftwards from the peak pixel, otherwise,
+rightwards.
+ 
+XXX: reverse order of row,col args?
+ 
+XXX: Input row/col are in image coords.
+ 
+XXX: The result is returned in image coords.
+*****************************************************************************/
+static psF32 findValue(pmSource *source,
+                       psF32 level,
+                       psU32 row,
+                       psU32 col,
+                       psU32 dir)
+{
+    psTrace("psModules.objects", 4, "---- %s() begin ----\n", __func__);
+    //
+    // Convert coords to subImage space.
+    //
+    psU32 subRow = row - source->pixels->row0;
+    psU32 subCol = col - source->pixels->col0;
+
+    // Ensure that the starting column is allowable.
+    if (!((0 <= subCol) && (subCol < source->pixels->numCols))) {
+        psError(PS_ERR_UNKNOWN, true, "Starting column outside subImage range");
+        psTrace("psModules.objects", 4, "---- %s(NAN) end ----\n", __func__);
+        return(NAN);
+    }
+    if (!((0 <= subRow) && (subRow < source->pixels->numRows))) {
+        psTrace("psModules.objects", 4, "---- %s(NAN) end ----\n", __func__);
+        psError(PS_ERR_UNKNOWN, true, "Starting row outside subImage range");
+        return(NAN);
+    }
+
+    // XXX EAM : i changed this to match pmModelEval above, but see
+    // XXX EAM   the note below in pmSourceContour
+    psF32 oldValue = pmModelEval(source->modelEXT, source->pixels, subCol, subRow);
+    if (oldValue == level) {
+        psTrace("psModules.objects", 4, "---- %s() end ----\n", __func__);
+        return(((psF32) (subCol + source->pixels->col0)));
+    }
+
+    //
+    // We define variables incr and lastColumn so that we can use the same loop
+    // whether we are stepping leftwards, or rightwards.
+    //
+    psS32 incr;
+    psS32 lastColumn;
+    if (dir == 0) {
+        incr = -1;
+        lastColumn = -1;
+    } else {
+        incr = 1;
+        lastColumn = source->pixels->numCols;
+    }
+    subCol+=incr;
+
+    while (subCol != lastColumn) {
+        psF32 newValue = pmModelEval(source->modelEXT, source->pixels, subCol, subRow);
+        if (oldValue == level) {
+            psTrace("psModules.objects", 4, "---- %s() end ----\n", __func__);
+            return((psF32) (subCol + source->pixels->col0));
+        }
+
+        if ((newValue <= level) && (level <= oldValue)) {
+            // This is simple linear interpolation.
+            psTrace("psModules.objects", 4, "---- %s() end ----\n", __func__);
+            return( ((psF32) (subCol + source->pixels->col0)) + ((psF32) incr) * ((level - newValue) / (oldValue - newValue)) );
+        }
+
+        if ((oldValue <= level) && (level <= newValue)) {
+            // This is simple linear interpolation.
+            psTrace("psModules.objects", 4, "---- %s() end ----\n", __func__);
+            return( ((psF32) (subCol + source->pixels->col0)) + ((psF32) incr) * ((level - oldValue) / (newValue - oldValue)) );
+        }
+
+        subCol+=incr;
+    }
+
+    psTrace("psModules.objects", 4, "---- %s(NAN) end ----\n", __func__);
+    return(NAN);
+}
+
+/******************************************************************************
+new implementation of source contour function
+*****************************************************************************/
+psArray *pmSourceContour (psImage *image, int xc, int yc, float threshold)
+{
+
+    int xR, yR, x0, x1, x0s, x1s;
+
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(image, false);
+
+    int x = xc - image->col0;
+    int y = yc - image->row0;
+
+    // the requested point must be within the contour
+    if (image->data.F32[y][x] < threshold)
+        return NULL;
+
+    // Ensure that the starting column is allowable.
+    if (x < 0)
+        return NULL;
+    if (y < 0)
+        return NULL;
+    if (x >= image->numCols)
+        return NULL;
+    if (y >= image->numRows)
+        return NULL;
+
+    // Allocate data for x/y pairs.
+    psVector *xVec = psVectorAllocEmpty(100, PS_TYPE_F32);
+    psVector *yVec = psVectorAllocEmpty(100, PS_TYPE_F32);
+
+    // First row: find the left and right end-points
+    int Npt = 0;
+
+    x0 = findContourNeg (image, threshold, x, y, LEFT);
+    x1 = findContourNeg (image, threshold, x, y, RIGHT);
+    xVec->data.F32[Npt + 0] = image->col0 + x0;
+    xVec->data.F32[Npt + 1] = image->col0 + x1;
+    yVec->data.F32[Npt + 0] = image->row0 + y;
+    yVec->data.F32[Npt + 1] = image->row0 + y;
+    Npt += 2;
+
+    x0s = x0;
+    x1s = x1;
+
+    // look for contour outline above row
+    xR = x0s;
+    yR = y + 1;
+    while (yR < image->numRows) {
+        if (image->data.F32[yR][xR] < threshold) {
+            x0 = findContourPos (image, threshold, xR, yR, RIGHT, x1);
+            if (x0 == x1) {
+                // fprintf (stderr, "top: %d (%d - %d)\n", yR, xR, x1);
+                goto pt1;
+            }
+            x1 = findContourNeg (image, threshold, x0, yR, RIGHT);
+            x0--;
+        } else {
+            x0 = findContourNeg (image, threshold, xR, yR, LEFT);
+            x1 = findContourNeg (image, threshold, xR, yR, RIGHT);
+        }
+        // fprintf (stderr, "pos: %d (%d - %d)\n", yR, x0, x1);
+
+        xVec->data.F32[Npt + 0] = image->col0 + x0;
+        xVec->data.F32[Npt + 1] = image->col0 + x1;
+        yVec->data.F32[Npt + 0] = image->row0 + yR;
+        yVec->data.F32[Npt + 1] = image->row0 + yR;
+        Npt += 2;
+
+        if (Npt >= xVec->nalloc - 1) {
+            psVectorRealloc (xVec, xVec->nalloc + 100);
+            psVectorRealloc (yVec, yVec->nalloc + 100);
+        }
+        yR ++;
+        xR = x0;
+    }
+
+pt1:
+    // look for contour outline below row
+    xR = x0s;
+    x1 = x1s;
+    yR = y - 1;
+    while (yR >= 0) {
+        if (image->data.F32[yR][xR] < threshold) {
+            x0 = findContourPos (image, threshold, xR, yR, RIGHT, x1);
+            if (x0 == x1) {
+                // fprintf (stderr, "top: %d (%d - %d)\n", yR, xR, x1);
+                goto pt2;
+            }
+            x1 = findContourNeg (image, threshold, x0, yR, RIGHT);
+            x0--;
+        } else {
+            x0 = findContourNeg (image, threshold, xR, yR, LEFT);
+            x1 = findContourNeg (image, threshold, xR, yR, RIGHT);
+        }
+        // fprintf (stderr, "neg: %d (%d - %d)\n", yR, x0, x1);
+
+        xVec->data.F32[Npt + 0] = image->col0 + x0;
+        xVec->data.F32[Npt + 1] = image->col0 + x1;
+        yVec->data.F32[Npt + 0] = image->row0 + yR;
+        yVec->data.F32[Npt + 1] = image->row0 + yR;
+        Npt += 2;
+
+        if (Npt >= xVec->nalloc - 1) {
+            psVectorRealloc (xVec, xVec->nalloc + 100);
+            psVectorRealloc (yVec, yVec->nalloc + 100);
+        }
+        yR --;
+    }
+pt2:
+    xVec->n = Npt;
+    yVec->n = Npt;
+
+    // fprintf (stderr, "done\n");
+    psArray *tmpArray = psArrayAlloc(2);
+
+    tmpArray->data[0] = (psPtr *) xVec;
+    tmpArray->data[1] = (psPtr *) yVec;
+    psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+    return(tmpArray);
+}
+
+/******************************************************************************
+    pmSourceContour(src, img, level, mode): For an input subImage, and model, this
+    routine returns a psArray of coordinates that evaluate to the specified level.
+ 
+    XXX: Probably should remove the "image" argument.
+    XXX: What type should the output coordinate vectors consist of?  col,row?
+    XXX: Why a pmArray output?
+    XXX: doex x,y correspond with col,row or row/col?
+    XXX: What is mode?
+    XXX: The top, bottom of the contour is not correctly determined.
+    XXX EAM : this function is using the model for the contour, but it should
+              be using only the image counts
+*****************************************************************************/
+psArray *pmSourceContour_Crude(pmSource *source,
+                               const psImage *image,
+                               psF32 level)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(image, false);
+    PS_ASSERT_PTR_NON_NULL(source->moments, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_PTR_NON_NULL(source->pixels, false);
+    PS_ASSERT_PTR_NON_NULL(source->modelEXT, false);
+    // XXX EAM : what is the purpose of modelPSF/modelEXT?
+
+    //
+    // Allocate data for x/y pairs.
+    //
+    psVector *xVec = psVectorAlloc(2 * source->pixels->numRows, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc(2 * source->pixels->numRows, PS_TYPE_F32);
+
+    //
+    // Start at the row with peak pixel, then decrement.
+    //
+    psS32 col = source->peak->x;
+    for (psS32 row = source->peak->y; row>= 0 ; row--) {
+        // XXX: yVec contain no real information.  Do we really need it?
+        yVec->data.F32[row] = (psF32) (source->pixels->row0 + row);
+        yVec->data.F32[row+yVec->n] = (psF32) (source->pixels->row0 + row);
+
+        // Starting at peak pixel, search leftwards for the column intercept.
+        psF32 leftIntercept = findValue(source, level, row, col, 0);
+        if (isnan(leftIntercept)) {
+            psError(PS_ERR_UNKNOWN, true, "Could not find contour edge (NAN)");
+            psFree(xVec);
+            psFree(yVec);
+            psTrace("psModules.objects", 3, "---- %s(NULL) end ----\n", __func__);
+            return(NULL);
+            //psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not find contour edge (NAN)\n");
+        }
+        xVec->data.F32[row] = ((psF32) source->pixels->col0) + leftIntercept;
+
+        // Starting at peak pixel, search rightwards for the column intercept.
+
+        psF32 rightIntercept = findValue(source, level, row, col, 1);
+        if (isnan(rightIntercept)) {
+            psError(PS_ERR_UNKNOWN, true, "Could not find contour edge (NAN)");
+            psFree(xVec);
+            psFree(yVec);
+            psTrace("psModules.objects", 3, "---- %s(NULL) end ----\n", __func__);
+            return(NULL);
+            //psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not find contour edge (NAN)\n");
+        }
+        psTrace("psModules.objects", 4, "The intercepts are (%.2f, %.2f)\n", leftIntercept, rightIntercept);
+        xVec->data.F32[row+xVec->n] = ((psF32) source->pixels->col0) + rightIntercept;
+
+        // Set starting column for next row
+        col = (psS32) ((leftIntercept + rightIntercept) / 2.0);
+    }
+    //
+    // Start at the row (+1) with peak pixel, then increment.
+    //
+    col = source->peak->x;
+    for (psS32 row = 1 + source->peak->y; row < source->pixels->numRows ; row++) {
+        // XXX: yVec contain no real information.  Do we really need it?
+        yVec->data.F32[row] = (psF32) (source->pixels->row0 + row);
+        yVec->data.F32[row+yVec->n] = (psF32) (source->pixels->row0 + row);
+
+        // Starting at peak pixel, search leftwards for the column intercept.
+        psF32 leftIntercept = findValue(source, level, row, col, 0);
+        if (isnan(leftIntercept)) {
+            psError(PS_ERR_UNKNOWN, true, "Could not find contour edge (NAN)");
+            psFree(xVec);
+            psFree(yVec);
+            psTrace("psModules.objects", 3, "---- %s(NULL) end ----\n", __func__);
+            return(NULL);
+            //psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not find contour edge (NAN)\n");
+        }
+        xVec->data.F32[row] = ((psF32) source->pixels->col0) + leftIntercept;
+
+        // Starting at peak pixel, search rightwards for the column intercept.
+        psF32 rightIntercept = findValue(source, level, row, col, 1);
+        if (isnan(rightIntercept)) {
+            psError(PS_ERR_UNKNOWN, true, "Could not find contour edge (NAN)");
+            psFree(xVec);
+            psFree(yVec);
+            psTrace("psModules.objects", 3, "---- %s(NULL) end ----\n", __func__);
+            return(NULL);
+            //psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not find contour edge (NAN)\n");
+        }
+        xVec->data.F32[row+xVec->n] = ((psF32) source->pixels->col0) + rightIntercept;
+
+        // Set starting column for next row
+        col = (psS32) ((leftIntercept + rightIntercept) / 2.0);
+    }
+
+    //
+    // Allocate an array for result, store coord vectors there.
+    //
+    psArray *tmpArray = psArrayAlloc(2);
+
+    tmpArray->data[0] = (psPtr *) yVec;
+    tmpArray->data[1] = (psPtr *) xVec;
+    psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+    return(tmpArray);
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceContour.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceContour.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceContour.h	(revision 15358)
@@ -0,0 +1,39 @@
+/* @file  pmSourceContour.h
+ *
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-01-24 02:54:15 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_SOURCE_CONTOUR_H
+# define PM_SOURCE_CONTOUR_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+psArray *pmSourceContour (psImage *image, int xc, int yc, float threshold);
+
+
+/** pmSourceContour()
+ *
+ * Find points in a contour for the given source at the given level. If type
+ * is PM_CONTOUR_CRUDE, the contour is found by starting at the source peak,
+ * running along each pixel row until the level is crossed, then interpolating to
+ * the level coordinate for that row. This is done for each row, with the
+ * starting point determined by the midpoint of the previous row, until the
+ * starting point has a value below the contour level. The returned contour
+ * consists of two vectors giving the x and y coordinates of the contour levels.
+ * This function may be used as part of the model guess inputs.  Other contour
+ * types may be specified in the future for more refined contours (TBD)
+ *
+ */
+psArray *pmSourceContour_Crude(
+    pmSource *source,   ///< The input pmSource
+    const psImage *image,  ///< The input image (float) (this arg should be removed)
+    float level   ///< The level of the contour
+);
+
+/// @}
+# endif /* PM_SOURCE_PHOTOMETRY_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitModel.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitModel.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitModel.c	(revision 15358)
@@ -0,0 +1,233 @@
+/** @file  pmSourceFitModel.c
+ *
+ *  fit single source models to image pixels
+ *
+ *  @author EAM, IfA
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.27 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-28 00:38:56 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourceFitModel.h"
+
+// save as static values so they may be set externally
+static psF32 PM_SOURCE_FIT_MODEL_NUM_ITERATIONS = 15;
+static psF32 PM_SOURCE_FIT_MODEL_TOLERANCE = 0.1;
+static psF32 PM_SOURCE_FIT_MODEL_WEIGHT = 1.0;
+static bool  PM_SOURCE_FIT_MODEL_PIX_WEIGHTS = true;
+
+bool pmSourceFitModelInit (float nIter, float tol, float weight, bool poissonErrors)
+{
+
+    PM_SOURCE_FIT_MODEL_NUM_ITERATIONS = nIter;
+    PM_SOURCE_FIT_MODEL_TOLERANCE = tol;
+    PM_SOURCE_FIT_MODEL_WEIGHT = weight;
+    PM_SOURCE_FIT_MODEL_PIX_WEIGHTS = poissonErrors;
+
+    return true;
+}
+
+bool pmSourceFitModel (pmSource *source,
+                       pmModel *model,
+                       pmSourceFitMode mode,
+                       psMaskType maskVal)
+{
+    psTrace("psModules.objects", 5, "---- %s begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(source->pixels, false);
+    PS_ASSERT_PTR_NON_NULL(source->maskObj, false);
+    PS_ASSERT_PTR_NON_NULL(source->weight, false);
+
+    psBool fitStatus = true;
+    psBool onPic     = true;
+    psBool rc        = true;
+
+    // maximum number of valid pixels
+    psS32 nPix = source->pixels->numRows * source->pixels->numCols;
+
+    // arrays to hold the data to be fitted
+    psArray *x = psArrayAllocEmpty(nPix);
+    psVector *y = psVectorAllocEmpty(nPix, PS_TYPE_F32);
+    psVector *yErr = psVectorAllocEmpty(nPix, PS_TYPE_F32);
+
+    // fill in the coordinate and value entries
+    nPix = 0;
+    for (psS32 i = 0; i < source->pixels->numRows; i++) {
+        for (psS32 j = 0; j < source->pixels->numCols; j++) {
+            // skip masked points
+            if (source->maskObj->data.U8[i][j] & maskVal) {
+                continue;
+            }
+            // skip zero-weight points
+            if (source->weight->data.F32[i][j] == 0) {
+                continue;
+            }
+            // skip nan values in image
+            if (!isfinite(source->pixels->data.F32[i][j])) {
+                continue;
+            }
+
+            psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+
+            // Convert i/j to image space:
+            coord->data.F32[0] = (psF32) (j + source->pixels->col0);
+            coord->data.F32[1] = (psF32) (i + source->pixels->row0);
+            x->data[nPix] = (psPtr *) coord;
+            y->data.F32[nPix] = source->pixels->data.F32[i][j];
+
+            // psMinimizeLMChi2 takes wt = 1/dY^2.  suggestion from RHL is to use the local sky
+            // as weight to avoid the bias from systematic errors here we would just use the
+            // source sky variance
+            if (PM_SOURCE_FIT_MODEL_PIX_WEIGHTS) {
+                yErr->data.F32[nPix] = 1.0 / source->weight->data.F32[i][j];
+            } else {
+                yErr->data.F32[nPix] = 1.0 / PM_SOURCE_FIT_MODEL_WEIGHT;
+            }
+            nPix++;
+        }
+    }
+    x->n = nPix;
+    y->n = nPix;
+    yErr->n = nPix;
+
+    psVector *params = model->params;
+    psVector *dparams = model->dparams;
+
+    // create the minimization constraints
+    psMinConstraint *constraint = psMinConstraintAlloc();
+    constraint->paramMask = psVectorAlloc (params->n, PS_TYPE_U8);
+    constraint->checkLimits = model->modelLimits;
+
+    // set parameter mask based on fitting mode
+    int nParams = 0;
+    switch (mode) {
+    case PM_SOURCE_FIT_NORM:
+        // NORM-only model fits only source normalization (Io)
+        nParams = 1;
+        psVectorInit (constraint->paramMask, 1);
+        constraint->paramMask->data.U8[PM_PAR_I0] = 0;
+        break;
+    case PM_SOURCE_FIT_PSF:
+        // PSF model only fits x,y,Io
+        nParams = 3;
+        psVectorInit (constraint->paramMask, 1);
+        constraint->paramMask->data.U8[PM_PAR_I0] = 0;
+        constraint->paramMask->data.U8[PM_PAR_XPOS] = 0;
+        constraint->paramMask->data.U8[PM_PAR_YPOS] = 0;
+        break;
+    case PM_SOURCE_FIT_EXT:
+        // EXT model fits all params (except sky)
+        nParams = params->n - 1;
+        psVectorInit (constraint->paramMask, 0);
+        constraint->paramMask->data.U8[PM_PAR_SKY] = 1;
+        break;
+    default:
+        psAbort("invalid fitting mode");
+    }
+    // force the floating parameters to fall within the contraint ranges
+    for (int i = 0; i < params->n; i++) {
+        model->modelLimits (PS_MINIMIZE_PARAM_MIN, i, params->data.F32, NULL);
+        model->modelLimits (PS_MINIMIZE_PARAM_MAX, i, params->data.F32, NULL);
+    }
+
+    if (nPix <  nParams + 1) {
+        psTrace ("psModules.objects", 4, "insufficient valid pixels\n");
+        model->flags |= PM_MODEL_STATUS_BADARGS;
+        psFree (x);
+        psFree (y);
+        psFree (yErr);
+        psFree (constraint);
+        return(false);
+    }
+
+    psMinimization *myMin = psMinimizationAlloc (PM_SOURCE_FIT_MODEL_NUM_ITERATIONS, PM_SOURCE_FIT_MODEL_TOLERANCE);
+
+    psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F32);
+
+    fitStatus = psMinimizeLMChi2(myMin, covar, params, constraint, x, y, yErr, model->modelFunc);
+    for (int i = 0; i < dparams->n; i++) {
+        if (psTraceGetLevel("psModules.objects") >= 4) {
+            fprintf (stderr, "%f ", params->data.F32[i]);
+        }
+        if ((constraint->paramMask != NULL) && constraint->paramMask->data.U8[i])
+            continue;
+        dparams->data.F32[i] = sqrt(covar->data.F32[i][i]);
+    }
+    psTrace ("psModules.objects", 4, "niter: %d, chisq: %f", myMin->iter, myMin->value);
+
+    // save the resulting chisq, nDOF, nIter
+    model->chisq = myMin->value;
+    model->nIter = myMin->iter;
+    model->nDOF  = y->n - nParams;
+    model->flags |= PM_MODEL_STATUS_FITTED;
+    if (!fitStatus) model->flags |= PM_MODEL_STATUS_NONCONVERGE;
+
+    // get the Gauss-Newton distance for fixed model parameters
+    // hold the fitted parameters fixed; mask sky which is not fitted at all
+    if (constraint->paramMask != NULL) {
+        psVector *delta = psVectorAlloc (params->n, PS_TYPE_F32);
+        psVector *altmask = psVectorAlloc (params->n, PS_TYPE_U8);
+        altmask->data.U8[0] = 1;
+        for (int i = 1; i < dparams->n; i++) {
+            altmask->data.U8[i] = (constraint->paramMask->data.U8[i]) ? 0 : 1;
+        }
+        psMinimizeGaussNewtonDelta(delta, params, altmask, x, y, yErr, model->modelFunc);
+
+        for (int i = 0; i < dparams->n; i++) {
+            if (!constraint->paramMask->data.U8[i])
+                continue;
+            // note that delta is the value *subtracted* from the parameter
+            // to get the new guess.  for dparams to represent the direction
+            // of motion, we need to take -delta
+            dparams->data.F32[i] = -delta->data.F32[i];
+        }
+        psFree (delta);
+        psFree (altmask);
+    }
+
+    // models can go insane: reject these
+    onPic &= (params->data.F32[PM_PAR_XPOS] >= source->pixels->col0);
+    onPic &= (params->data.F32[PM_PAR_XPOS] <  source->pixels->col0 + source->pixels->numCols);
+    onPic &= (params->data.F32[PM_PAR_YPOS] >= source->pixels->row0);
+    onPic &= (params->data.F32[PM_PAR_YPOS] <  source->pixels->row0 + source->pixels->numRows);
+    if (!onPic) {
+        model->flags |= PM_MODEL_STATUS_OFFIMAGE;
+    }
+
+    source->mode |= PM_SOURCE_MODE_FITTED;
+
+    psFree(x);
+    psFree(y);
+    psFree(yErr);
+    psFree(myMin);
+    psFree(covar);
+    psFree(constraint);
+
+    rc = (onPic && fitStatus);
+    psTrace("psModules.objects", 5, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitModel.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitModel.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitModel.h	(revision 15358)
@@ -0,0 +1,78 @@
+/* @file  pmSourceFitModel.h
+ *
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-06-20 02:22:26 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_SOURCE_FIT_MODEL_H
+# define PM_SOURCE_FIT_MODEL_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef enum {
+    PM_SOURCE_FIT_NORM,
+    PM_SOURCE_FIT_PSF,
+    PM_SOURCE_FIT_EXT,
+    PM_SOURCE_FIT_PSF_AND_SKY,
+    PM_SOURCE_FIT_EXT_AND_SKY
+} pmSourceFitMode;
+
+bool pmSourceFitModelInit(
+    float nIter,   ///< max number of allowed iterations
+    float tol,      ///< convergence criterion
+    float weight,      ///< use this weight for constant-weight fits
+    bool poissonErrors   // use poisson errors for fits?
+);
+
+/** pmSourceFitModel()
+ *
+ * Fit the requested model to the specified source. The starting guess for the model is given
+ * by the input source.model parameter values. The pixels of interest are specified by the
+ * source.pixels and source.mask entries. This function calls psMinimizeLMChi2() on the image
+ * data. The function returns TRUE on success or FALSE on failure.
+ *
+ */
+bool pmSourceFitModel(
+    pmSource *source,   ///< The input pmSource
+    pmModel *model,   ///< model to be fitted
+    pmSourceFitMode mode,  ///< define parameters to be fitted
+    psMaskType maskVal                  ///< Value to mask
+);
+
+
+// initialize data for a group of object models
+bool pmSourceFitSetInit (pmModelType type);
+
+// clear data for a group of object models
+void pmSourceFitSetClear (void);
+
+// function used to set limits for a group of models
+bool pmSourceFitSet_CheckLimits (psMinConstraintMode mode, int nParam, float *params, float *betas);
+
+// function used to fit a group of object models
+psF32 pmSourceFitSet_Function(psVector *deriv,
+                              const psVector *params,
+                              const psVector *x);
+
+/** pmSourceFitSet()
+ *
+ * Fit the requested model to the specified source. The starting guess for the model is given
+ * by the input source.model parameter values. The pixels of interest are specified by the
+ * source.pixels and source.mask entries. This function calls psMinimizeLMChi2() on the image
+ * data. The function returns TRUE on success or FALSE on failure.
+ *
+ */
+bool pmSourceFitSet(
+    pmSource *source,   ///< The input pmSource
+    psArray *modelSet,   ///< model to be fitted
+    pmSourceFitMode mode,  ///< define parameters to be fitted
+    psMaskType maskVal                  ///< Vale to mask
+
+);
+
+/// @}
+# endif /* PM_SOURCE_FIT_MODEL_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitSet.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitSet.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitSet.c	(revision 15358)
@@ -0,0 +1,455 @@
+/** @file  pmSourceFitModel.c
+ *
+ *  fit single source models to image pixels
+ *
+ *  @author EAM, IfA
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-28 00:39:41 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourceFitModel.h"
+#include "pmSourceFitSet.h"
+
+// save as static values so they may be set externally
+static psF32 PM_SOURCE_FIT_MODEL_NUM_ITERATIONS = 15;
+static psF32 PM_SOURCE_FIT_MODEL_TOLERANCE = 0.1;
+static psF32 PM_SOURCE_FIT_MODEL_WEIGHT = 1.0;
+static bool  PM_SOURCE_FIT_MODEL_PIX_WEIGHTS = true;
+
+/********************* Source Model Set Functions ***************************/
+
+static pmSourceFitSetData *thisSet = NULL;
+
+static void pmSourceFitSetDataFree (pmSourceFitSetData *set) {
+    if (!set) return;
+
+    psFree (set->modelSet);
+    psFree (set->paramSet);
+    psFree (set->derivSet);
+    return;
+}
+
+pmSourceFitSetData *pmSourceFitSetDataAlloc (psArray *modelSet) {
+
+    pmSourceFitSetData *set = (pmSourceFitSetData *) psAlloc(sizeof(pmSourceFitSetData));
+    psMemSetDeallocator(set, (psFreeFunc) pmSourceFitSetDataFree);
+
+    set->modelSet = psMemIncrRefCounter (modelSet);
+    set->paramSet = psArrayAlloc (modelSet->n);
+    set->derivSet = psArrayAlloc (modelSet->n);
+    set->nParamSet = 0;
+
+    for (int i = 0; i < modelSet->n; i++) {
+        pmModel *model = modelSet->data[i];
+
+        int nParams = pmModelClassParameterCount (model->type);
+
+        set->paramSet->data[i] = psVectorCopy (NULL, model->params, PS_DATA_F32);
+        set->derivSet->data[i] = psVectorAlloc (nParams, PS_DATA_F32);
+        psVectorInit (set->derivSet->data[i], 0.0);
+
+        set->nParamSet += nParams;
+    }
+
+    return set;
+}
+
+// this function is called with the full set of parameters and the beta values in a single vector
+bool pmSourceFitSetCheckLimits (psMinConstraintMode mode, int nParam, float *params, float *betas)
+{
+    assert (thisSet);
+
+    // nParam is the parameter in the full sequence.  determine which single model this comes from
+    int nModel = -1;
+    int nParamOne = -1;
+    int nParamBase = 0;
+    for (int i = 0; i < thisSet->modelSet->n; i++) {
+        psVector *param = thisSet->paramSet->data[i];
+        if ((nParamBase <= nParam) && (nParam < nParamBase + param->n)) {
+            nModel = i;
+            nParamOne = nParam - nParamBase;
+            break;
+        }
+        nParamBase += param->n;
+    }
+    assert (nModel > -1);
+
+    pmModel *model = thisSet->modelSet->data[nModel];
+
+    // pass the single model function a pointer to the start of that model's sequence
+    float *paramOne = params + nParamBase;
+    float *betaOne = betas + nParamBase;
+    bool status = model->modelLimits (mode, nParamOne, paramOne, betaOne);
+    return status;
+}
+
+// merge parameters from FitSet models into single param and deriv vectors
+bool pmSourceFitSetJoin (psVector *deriv, psVector *param, pmSourceFitSetData *set) {
+
+    assert (set);
+    assert (set->paramSet->n == set->derivSet->n);
+
+    int n = 0;
+    for (int i = 0; i < set->paramSet->n; i++) {
+
+        psVector *paramOne = set->paramSet->data[i];
+        psVector *derivOne = set->derivSet->data[i];
+
+	// one or the other (param or deriv) must be set
+	assert ((deriv != NULL) || (param != NULL));
+        assert ((deriv == NULL) || (param == NULL) || (paramOne->n == derivOne->n));
+	
+        for (int j = 0; j < paramOne->n; j++, n++) {
+	    if (param) {
+		param->data.F32[n] = paramOne->data.F32[j];
+	    }
+            if (deriv) {
+                deriv->data.F32[n] = derivOne->data.F32[j];
+            }
+        }
+    }
+    return true;
+}
+
+// distribute parameters from single param and deriv vectors into FitSet models
+bool pmSourceFitSetSplit (pmSourceFitSetData *set, const psVector *deriv, const psVector *param) {
+
+    assert (set);
+    assert (set->paramSet->n == set->derivSet->n);
+
+    int n = 0;
+    for (int i = 0; i < set->paramSet->n; i++) {
+
+        psVector *paramOne = set->paramSet->data[i];
+        psVector *derivOne = set->derivSet->data[i];
+        assert ((deriv == NULL) || (paramOne->n == derivOne->n));
+
+        for (int j = 0; j < paramOne->n; j++, n++) {
+            paramOne->data.F32[j] = param->data.F32[n];
+            if (deriv) {
+                derivOne->data.F32[j] = deriv->data.F32[n];
+            }
+        }
+    }
+    return true;
+}
+
+bool pmSourceFitSetValues (pmSourceFitSetData *set, const psVector *dparam, const psVector *param, pmSource *source, psMinimization *myMin, int nPix, bool fitStatus) {
+
+    bool onPic = true;
+
+    assert (set);
+
+    int n = 0;
+    for (int i = 0; i < set->paramSet->n; i++) {
+
+        pmModel *model = set->modelSet->data[i];
+
+        for (int j = 0; j < model->params->n; j++, n++) {
+            if (psTraceGetLevel("psModules.objects") >= 4) {
+                fprintf (stderr, "%f ", param->data.F32[n]);
+            }
+            model->params->data.F32[j] = param->data.F32[n];
+            model->dparams->data.F32[j] = dparam->data.F32[n];
+        }
+        psTrace ("psModules.objects", 4, " src %d", i);
+
+        // save the resulting chisq, nDOF, nIter
+        // these are not unique for any one source
+        model->chisq = myMin->value;
+        model->nIter = myMin->iter;
+        model->nDOF  = nPix - model->params->n;
+
+        // set the model success or failure status
+        model->flags |= PM_MODEL_STATUS_FITTED;
+        if (!fitStatus) model->flags |= PM_MODEL_STATUS_NONCONVERGE;
+
+        // models can go insane: reject these
+        onPic &= (model->params->data.F32[PM_PAR_XPOS] >= source->pixels->col0);
+        onPic &= (model->params->data.F32[PM_PAR_XPOS] <  source->pixels->col0 + source->pixels->numCols);
+        onPic &= (model->params->data.F32[PM_PAR_XPOS] >= source->pixels->row0);
+        onPic &= (model->params->data.F32[PM_PAR_XPOS] <  source->pixels->row0 + source->pixels->numRows);
+        if (!onPic) model->flags |= PM_MODEL_STATUS_OFFIMAGE;
+    }
+    return true;
+}
+
+// generic psMinLMM-style function for fitting: split the parameters across the models, call
+// each model function one-at-a-time, the join the derivatives for on-going evaluation
+psF32 pmSourceFitSetFunction(psVector *deriv, const psVector *param, const psVector *x)
+{
+    float chisqSum = 0.0;
+    float chisqOne = 0.0;
+
+    assert (thisSet);
+    pmSourceFitSetSplit (thisSet, deriv, param);
+
+    for (int i = 0; i < thisSet->modelSet->n; i++) {
+
+        pmModel *model = thisSet->modelSet->data[i];
+
+        psVector *paramOne = thisSet->paramSet->data[i];
+        psVector *derivOne = thisSet->derivSet->data[i];
+
+        chisqOne = model->modelFunc (derivOne, paramOne, x);
+        chisqSum += chisqOne;
+    }
+    pmSourceFitSetJoin (deriv, NULL, thisSet);
+
+    return (chisqSum);
+}
+
+// XXX allow the mode to be a function of the object (eg, S/N)
+bool pmSourceFitSetMasks (psMinConstraint *constraint, pmSourceFitSetData *set, pmSourceFitMode mode) {
+
+    // unmask everyone
+    psVectorInit (constraint->paramMask, 0);
+
+    int n = 0;
+    for (int i = 0; i < set->paramSet->n; i++) {
+        psVector *paramOne = set->paramSet->data[i];
+
+        switch (mode) {
+          case PM_SOURCE_FIT_NORM:
+            // mask all but Xo,Yo,Io
+            for (int j = 0; j < paramOne->n; j++) {
+                if (j == PM_PAR_I0) continue;
+                constraint->paramMask->data.U8[n + j] = 1;
+            }
+            break;
+          case PM_SOURCE_FIT_PSF:
+            // mask all but Xo,Yo,Io
+            for (int j = 0; j < paramOne->n; j++) {
+                if (j == PM_PAR_XPOS) continue;
+                if (j == PM_PAR_YPOS) continue;
+                if (j == PM_PAR_I0) continue;
+                constraint->paramMask->data.U8[n + j] = 1;
+            }
+            break;
+          case PM_SOURCE_FIT_EXT:
+            // EXT model fits all params (except sky)
+            constraint->paramMask->data.U8[n + PM_PAR_SKY] = 1;
+            break;
+          default:
+            psAbort("invalid fitting mode");
+        }
+        n += paramOne->n;
+    }
+    return true;
+}
+
+bool pmSourcePrintModelSet (FILE *file, psArray *modelSet) {
+
+    for (int i = 0; i < modelSet->n; i++) {
+	pmModel *model = modelSet->data[i];
+	int nParams = pmModelClassParameterCount (model->type);
+	for (int j = 0; j < nParams; j++) {
+	    fprintf (file, "%d %d  : %f %f\n", i, j, model->params->data.F32[j], model->dparams->data.F32[j]);
+	}
+    }
+    return true;
+}
+
+bool pmSourceFitSetPrint (FILE *file, pmSourceFitSetData *set) {
+
+    for (int i = 0; i < set->paramSet->n; i++) {
+        psVector *paramOne = set->paramSet->data[i];
+        psVector *derivOne = set->derivSet->data[i];
+        for (int j = 0; j < paramOne->n; j++) {
+	    fprintf (file, "%d %d  : %f %f\n", i, j, paramOne->data.F32[j], derivOne->data.F32[j]);
+        }
+    }
+    return true;
+}
+
+bool pmSourceFitSet (pmSource *source,
+                     psArray *modelSet,
+                     pmSourceFitMode mode,
+                     psMaskType maskVal)
+{
+    psTrace("psModules.objects", 3, "---- %s begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(source->pixels, false);
+    PS_ASSERT_PTR_NON_NULL(source->maskObj, false);
+    PS_ASSERT_PTR_NON_NULL(source->weight, false);
+
+    bool fitStatus = true;
+    bool onPic     = true;
+
+    // maximum number of valid pixels
+    int nPix = source->pixels->numRows * source->pixels->numCols;
+
+    // construct the coordinate and value entries
+    psArray *x = psArrayAllocEmpty(nPix);
+    psVector *y = psVectorAllocEmpty(nPix, PS_TYPE_F32);
+    psVector *yErr = psVectorAllocEmpty(nPix, PS_TYPE_F32);
+
+    // fill in the coordinate and value entries
+    nPix = 0;
+    for (psS32 i = 0; i < source->pixels->numRows; i++) {
+        for (psS32 j = 0; j < source->pixels->numCols; j++) {
+            // skip masked points
+            if (source->maskObj->data.U8[i][j] & maskVal) {
+                continue;
+            }
+            // skip zero-weight points
+            if (source->weight->data.F32[i][j] == 0) {
+                continue;
+            }
+            // skip nan values in image
+            if (!isfinite(source->pixels->data.F32[i][j])) {
+                continue;
+            }
+
+            psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+
+            // Convert i/j to image space:
+            coord->data.F32[0] = (psF32) (j + source->pixels->col0);
+            coord->data.F32[1] = (psF32) (i + source->pixels->row0);
+            x->data[nPix] = (psPtr *) coord;
+            y->data.F32[nPix] = source->pixels->data.F32[i][j];
+
+            // psMinimizeLMChi2 takes wt = 1/dY^2.  suggestion from RHL is to use the local sky
+            // as weight to avoid the bias from systematic errors here we would just use the
+            // source sky variance
+            if (PM_SOURCE_FIT_MODEL_PIX_WEIGHTS) {
+                yErr->data.F32[nPix] = 1.0 / source->weight->data.F32[i][j];
+            } else {
+                yErr->data.F32[nPix] = 1.0 / PM_SOURCE_FIT_MODEL_WEIGHT;
+            }
+            nPix++;
+        }
+    }
+    x->n = nPix;
+    y->n = nPix;
+    yErr->n = nPix;
+
+    // create the FitSet and set the initial parameter guesses
+    thisSet = pmSourceFitSetDataAlloc (modelSet);
+
+    // define param and deriv vectors for complete set of parameters
+    psVector *params = psVectorAlloc (thisSet->nParamSet, PS_TYPE_F32);
+
+    // set the param and deriv vectors based on the curent values
+    pmSourceFitSetJoin (NULL, params, thisSet);
+
+    // create the minimization constraints
+    psMinConstraint *constraint = psMinConstraintAlloc();
+    constraint->paramMask = psVectorAlloc (thisSet->nParamSet, PS_TYPE_U8);
+    constraint->checkLimits = pmSourceFitSetCheckLimits;
+
+    pmSourceFitSetMasks (constraint, thisSet, mode);
+
+    // force the floating parameters to fall within the contraint ranges
+    for (int i = 0; i < params->n; i++) {
+        pmSourceFitSetCheckLimits (PS_MINIMIZE_PARAM_MIN, i, params->data.F32, NULL);
+        pmSourceFitSetCheckLimits (PS_MINIMIZE_PARAM_MAX, i, params->data.F32, NULL);
+    }
+
+    if (psTraceGetLevel("psModules.objects") >= 5) {
+        for (int i = 0; i < params->n; i++) {
+            fprintf (stderr, "%d %f %d\n", i, params->data.F32[i], constraint->paramMask->data.U8[i]);
+        }
+    }
+
+    if (nPix <  thisSet->nParamSet + 1) {
+        psTrace (__func__, 4, "insufficient valid pixels\n");
+        psTrace("psModules.objects", 3, "---- %s() end : fail pixels ----\n", __func__);
+        for (int i = 0; i < modelSet->n; i++) {
+            pmModel *model = modelSet->data[i];
+            model->flags |= PM_MODEL_STATUS_BADARGS;
+        }
+        psFree (x);
+        psFree (y);
+        psFree (yErr);
+        psFree (params);
+        psFree(constraint);
+        psFree (thisSet);
+        thisSet = NULL;
+        return(false);
+    }
+
+    psMinimization *myMin = psMinimizationAlloc (PM_SOURCE_FIT_MODEL_NUM_ITERATIONS, PM_SOURCE_FIT_MODEL_TOLERANCE);
+
+    psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F32);
+
+    fitStatus = psMinimizeLMChi2(myMin, covar, params, constraint, x, y, yErr, pmSourceFitSetFunction);
+    if (!fitStatus) {
+        psTrace("psModules.objects", 4, "Failed to fit model (%ld components)\n", modelSet->n);
+    }
+
+    // parameter errors from the covariance matrix
+    psVector *dparams = psVectorAlloc (thisSet->nParamSet, PS_TYPE_F32);
+    for (int i = 0; i < dparams->n; i++) {
+        if ((constraint->paramMask != NULL) && constraint->paramMask->data.U8[i])
+            continue;
+        dparams->data.F32[i] = sqrt(covar->data.F32[i][i]);
+    }
+
+    // get the Gauss-Newton distance for fixed model parameters
+    if (constraint->paramMask != NULL) {
+        psVector *delta = psVectorAlloc (params->n, PS_TYPE_F32);
+        psVector *altmask = psVectorAlloc (params->n, PS_TYPE_U8);
+        altmask->data.U8[0] = 1;
+        for (int i = 1; i < dparams->n; i++) {
+            altmask->data.U8[i] = (constraint->paramMask->data.U8[i]) ? 0 : 1;
+        }
+        psMinimizeGaussNewtonDelta(delta, params, altmask, x, y, yErr, pmSourceFitSetFunction);
+
+        for (int i = 0; i < dparams->n; i++) {
+            if (!constraint->paramMask->data.U8[i])
+                continue;
+            // note that delta is the value *subtracted* from the parameter
+            // to get the new guess.  for dparams to represent the direction
+            // of motion, we need to take -delta
+            dparams->data.F32[i] = -delta->data.F32[i];
+        }
+        psFree (delta);
+        psFree (altmask);
+    }
+
+    pmSourceFitSetValues (thisSet, dparams, params, source, myMin, y->n, fitStatus);
+    psTrace ("psModules.objects", 5, "onPic: %d, fitStatus: %d, nIter: %d, chisq: %f, nPix: %ld\n", onPic, fitStatus, myMin->iter, myMin->value, y->n);
+
+    source->mode |= PM_SOURCE_MODE_FITTED;
+
+    psFree(x);
+    psFree(y);
+    psFree(yErr);
+    psFree(myMin);
+    psFree(covar);
+    psFree(constraint);
+    psFree(params);
+    psFree(dparams);
+    psFree(thisSet);
+
+    thisSet = NULL;
+
+    bool rc = (onPic && fitStatus);
+    psTrace("psModules.objects", 5, "---- %s end (%d) ----\n", __func__, rc);
+    return(rc);
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitSet.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitSet.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceFitSet.h	(revision 15358)
@@ -0,0 +1,56 @@
+/* @file  pmSourceFitSet.h
+ *
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-09-28 00:39:47 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_SOURCE_FIT_SET_H
+# define PM_SOURCE_FIT_SET_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef struct {
+    psArray *modelSet;
+    psArray *paramSet;
+    psArray *derivSet;
+    int nParamSet;
+} pmSourceFitSetData;
+
+// initialize data for a group of object models
+pmSourceFitSetData *pmSourceFitSetDataAlloc (psArray *modelSet);
+
+// function used to set limits for a group of models
+bool pmSourceFitSetCheckLimits (psMinConstraintMode mode, int nParam, float *params, float *betas);
+
+bool pmSourceFitSetJoin (psVector *deriv, psVector *param, pmSourceFitSetData *set);
+bool pmSourceFitSetSplit (pmSourceFitSetData *set, const psVector *deriv, const psVector *param);
+bool pmSourceFitSetValues (pmSourceFitSetData *set, const psVector *dparam, const psVector *param, pmSource *source, psMinimization *myMin, int nPix, bool fitStatus);
+
+psF32 pmSourceFitSetFunction(psVector *deriv, const psVector *param, const psVector *x);
+bool pmSourceFitSetMasks (psMinConstraint *constraint, pmSourceFitSetData *set, pmSourceFitMode mode);
+
+/** pmSourceFitSet()
+ *
+ * Fit the requested model to the specified source. The starting guess for the model is given
+ * by the input source.model parameter values. The pixels of interest are specified by the
+ * source.pixels and source.mask entries. This function calls psMinimizeLMChi2() on the image
+ * data. The function returns TRUE on success or FALSE on failure.
+ *
+ */
+bool pmSourceFitSet(
+    pmSource *source,   ///< The input pmSource
+    psArray *modelSet,   ///< model to be fitted
+    pmSourceFitMode mode,  ///< define parameters to be fitted
+    psMaskType maskVal                  ///< Vale to mask
+
+);
+
+bool pmSourcePrintModelSet (FILE *file, psArray *modelSet);
+bool pmSourceFitSetPrint (FILE *file, pmSourceFitSetData *set);
+
+/// @}
+# endif /* PM_SOURCE_FIT_MODEL_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO.c	(revision 15358)
@@ -0,0 +1,852 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.52 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-10-05 22:46:34 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <strings.h>            /* for strn?casecmp */
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmConcepts.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourceIO.h"
+
+#define BLANK_HEADERS "BLANK.HEADERS"   // Name of metadata in camera configuration containing header names
+                                        // for putting values into a blank PHU
+
+// translations between psphot object types and dophot object types
+int pmSourceGetDophotType (pmSource *source)
+{
+    switch (source->type) {
+
+      case PM_SOURCE_TYPE_DEFECT:
+      case PM_SOURCE_TYPE_SATURATED:
+        return (8);
+
+      case PM_SOURCE_TYPE_STAR:
+        if (source->mode & PM_SOURCE_MODE_SATSTAR)
+            return (10);
+        if (source->mode & PM_SOURCE_MODE_POOR)
+            return (7);
+        if (source->mode & PM_SOURCE_MODE_FAIL)
+            return (4);
+        return (1);
+
+      case PM_SOURCE_TYPE_EXTENDED:
+        return (2);
+
+      default:
+        return (0);
+    }
+    return (0);
+}
+
+// translations between psphot object types and dophot object types
+bool pmSourceSetDophotType (pmSource *source, int type)
+{
+    if (type == 4) {
+        source->mode |= PM_SOURCE_MODE_FAIL;
+    }
+    if (type == 7) {
+        source->mode |= PM_SOURCE_MODE_POOR;
+    }
+    if (type == 10) {
+        source->mode |= PM_SOURCE_MODE_SATSTAR;
+    }
+
+    switch (type) {
+      case 1:
+      case 4:
+      case 7:
+      case 10:
+        source->type = PM_SOURCE_TYPE_STAR;
+        return true;
+      case 2:
+        source->type = PM_SOURCE_TYPE_EXTENDED;
+        return true;
+      case 8:
+        source->type = PM_SOURCE_TYPE_DEFECT;
+        return true;
+      default:
+        return false;
+    }
+    return false;
+}
+
+// Given a FITS file pointer, write the table of object data
+bool pmFPAviewWriteObjects (const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        if (!pmFPAWriteObjects (fpa, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write objects from fpa");
+            return false;
+        }
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing chip == %d (>= chips->n == %ld)", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        if (!pmChipWriteObjects (chip, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write objects from chip");
+            return false;
+        }
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing cell == %d (>= cells->n == %ld)",
+                view->cell, chip->cells->n);
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        if (!pmCellWriteObjects (cell, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write objects from cell");
+            return false;
+        }
+
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing readout == %d (>= readouts->n == %ld)",
+                view->readout, cell->readouts->n);
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    if (!pmReadoutWriteObjects (readout, view, file, config)) {
+        psError(PS_ERR_IO, false, "Failed to write objects from readout %d", view->readout);
+        return false;
+    }
+
+    return true;
+}
+
+// read in all chip-level Objects files for this FPA
+bool pmFPAWriteObjects (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        if (!pmChipWriteObjects (chip, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth chip", i);
+            psFree (thisView);
+            return false;
+        }
+    }
+    psFree (thisView);
+    return true;
+}
+
+// read in all cell-level Objects files for this chip
+bool pmChipWriteObjects (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        thisView->cell = i;
+        if (!pmCellWriteObjects (cell, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth cell", i);
+            psFree (thisView);
+            return false;
+        }
+    }
+    psFree (thisView);
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmCellWriteObjects (pmCell *cell, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        thisView->readout = i;
+        if (!pmReadoutWriteObjects (readout, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth readout", i);
+            psFree (thisView);
+            return false;
+        }
+    }
+    psFree (thisView);
+    return true;
+}
+
+// write out all readout-level Objects files for this cell
+bool pmReadoutWriteObjects (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+
+    bool status;
+    char *exttype;
+    char *dataname;
+    char *headname;
+    pmHDU *hdu;
+    psMetadata *updates;
+    psMetadata *outhead;
+
+    // XXX if sources is NULL, skip the cell or write out empty tables?
+    psArray *sources = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.SOURCES");
+    if (sources == NULL)
+        return true;
+
+    switch (file->type) {
+      case PM_FPA_FILE_RAW:
+        pmSourcesWriteRAW (sources, file->filename);
+        break;
+
+      case PM_FPA_FILE_OBJ:
+        pmSourcesWriteOBJ (sources, file->filename);
+        break;
+
+      case PM_FPA_FILE_SX:
+        pmSourcesWriteSX (sources, file->filename);
+        break;
+
+      case PM_FPA_FILE_CMP:
+        // a SPLIT format : only one header and object table per file
+        hdu = pmFPAviewThisHDU (view, file->fpa);
+
+        // copy the header to an output header, add the output header data
+        outhead = psMetadataCopy (NULL, hdu->header);
+
+        // copy over the entries saved by PSPHOT
+        updates = psMetadataLookupPtr (NULL, readout->analysis, "PSPHOT.HEADER");
+        if (updates) {
+            psMetadataCopy (outhead, updates);
+        }
+
+        // copy over the entries saved by PSASTRO
+        updates = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.HEADER");
+        if (updates) {
+            psMetadataCopy (outhead, updates);
+        }
+
+        bool status = pmSourcesWriteCMP (sources, file->filename, outhead);
+        psFree (outhead);
+
+        if (!status) {
+            psError(PS_ERR_IO, false, "Failed to write CMP file\n");
+            return false;
+        }
+        break;
+
+      case PM_FPA_FILE_CMF:
+        // write a header? (only if this is the first readout for cell)
+        //   note that the file->header is set to track the last hdu->header written
+        // write the data? (always?)
+
+        // get the current header
+        hdu = pmFPAviewThisHDU (view, file->fpa);
+
+        // define the EXTNAME values for the different data segments:
+        {
+            // lookup the EXTNAME values used for table data and image header segments
+            char *rule = NULL;
+
+            // Menu of EXTNAME rules
+            psMetadata *menu = psMetadataLookupMetadata(&status, file->camera, "EXTNAME.RULES");
+            if (!menu) {
+                psError(PS_ERR_UNKNOWN, true, "missing EXTNAME.RULES in camera.config");
+                return false;
+            }
+
+            // EXTNAME for image header
+            rule = psMetadataLookupStr(&status, menu, "CMF.HEAD");
+            if (!rule) {
+                psError(PS_ERR_UNKNOWN, true, "missing entry for CMF.HEAD in EXTNAME.RULES in camera.config");
+                return false;
+            }
+            headname = pmFPAfileNameFromRule (rule, file, view);
+
+            // EXTNAME for table data
+            rule = psMetadataLookupStr(&status, menu, "CMF.DATA");
+            if (!rule) {
+                psError(PS_ERR_UNKNOWN, true, "missing entry for CMF.DATA in EXTNAME.RULES in camera.config");
+                return false;
+            }
+            dataname = pmFPAfileNameFromRule (rule, file, view);
+        }
+
+        // write out the IMAGE header segment (only for the first readout of the cell)
+        {
+            // this header block is new, write it to disk
+            if (hdu->header != file->header) {
+                // add EXTNAME, EXTHEAD, EXTTYPE to header
+                psMetadataAddStr (hdu->header, PS_LIST_TAIL, "EXTDATA", PS_META_REPLACE, "name of table extension", dataname);
+                psMetadataAddStr (hdu->header, PS_LIST_TAIL, "EXTTYPE", PS_META_REPLACE, "extension type", "IMAGE");
+                if (!file->wrote_phu) {
+                    // this hdu->header acts as the PHU: set EXTEND to be true
+                    psMetadataAddBool (hdu->header, PS_LIST_TAIL, "EXTEND", PS_META_REPLACE, "this file has extensions", true);
+                    file->wrote_phu = true;
+                }
+
+                // save psphot and psastro metadata in the image and table headers
+                updates = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.HEADER");
+                if (updates) {
+                    psMetadataCopy (hdu->header, updates);
+                }
+                updates = psMetadataLookupPtr (&status, readout->analysis, "PSASTRO.HEADER");
+                if (updates) {
+                    psMetadataCopy (hdu->header, updates);
+                }
+
+                pmConfigConformHeader(hdu->header, file->format);
+
+                psFitsWriteBlank (file->fits, hdu->header, headname);
+                psTrace ("pmFPAfile", 5, "wrote ext head %s (type: %d)\n", file->filename, file->type);
+                file->header = hdu->header;
+            }
+        }
+
+        // write out the TABLE data segment
+        {
+            // create a header to hold the output data
+            outhead = psMetadataAlloc ();
+
+            // determine the output table format
+            psMetadata *recipe = psMetadataLookupMetadata(&status, config->recipes, "PSPHOT");
+            if (!status) {
+                psError(PS_ERR_UNKNOWN, true, "missing recipe PSPHOT in config data");
+                return false;
+            }
+            exttype = psMemIncrRefCounter (psMetadataLookupStr(&status, recipe, "OUTPUT.FORMAT"));
+            if (!exttype) {
+                exttype = psStringCopy ("SMPDATA");
+            }
+
+            // write the links to the image header
+            psMetadataAddStr (outhead, PS_LIST_TAIL, "EXTHEAD", PS_META_REPLACE, "name of image extension w/", headname);
+            psMetadataAddStr (outhead, PS_LIST_TAIL, "EXTTYPE", PS_META_REPLACE, "extension type", exttype);
+            psFree (exttype);
+
+            // XXX these are case-sensitive since the EXTYPE is case-sensitive
+            status = false;
+            if (!strcmp (exttype, "SMPDATA")) {
+                status = pmSourcesWrite_SMPDATA (file->fits, sources, file->header, outhead, dataname);
+            }
+            if (!strcmp (exttype, "PS1_DEV_0")) {
+                status = pmSourcesWrite_PS1_DEV_0 (file->fits, sources, file->header, outhead, dataname);
+            }
+            if (!strcmp (exttype, "PS1_DEV_1")) {
+                status = pmSourcesWrite_PS1_DEV_1 (file->fits, sources, file->header, outhead, dataname);
+            }
+
+            if (!status) {
+                psError(PS_ERR_IO, false, "writing CMF data to %s with format %s\n", file->filename, exttype);
+                psFree (headname);
+                psFree (dataname);
+                psFree (outhead);
+                return false;
+            }
+        }
+
+        psTrace ("pmFPAfile", 5, "wrote ext data %s (type: %d)\n", file->filename, file->type);
+
+        psFree (headname);
+        psFree (dataname);
+        psFree (outhead);
+        break;
+
+      default:
+        fprintf (stderr, "warning: type mismatch\n");
+        break;
+    }
+    return true;
+}
+// a MEF CMF file has: PHU, CELL-HEAD, TABLE, CELL-HEAD, TABLE, TABLE, TABLE...
+
+// if this file needs to have a PHU written out, write one
+bool pmSource_CMF_WritePHU (const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    bool status;
+
+    // not needed if already written
+    if (file->wrote_phu) return true;
+
+    // not needed if not FPA
+    // XXX this prevents us from defining a SPLIT/MEF CMF file...
+    if (file->fileLevel != PM_FPA_LEVEL_FPA) return true;
+
+    // not needed if only one chip
+    if (file->fpa->chips->n == 1) return true;
+
+    // find the FPA phu
+    pmHDU *phu = pmFPAviewThisPHU (view, file->fpa);
+
+    // if there is no PHU, this is a single header+image (extension-less) file. This could be
+    // the case for an input SPLIT set of files being written out as a MEF.  if there is a PHU,
+    // write it out as a 'blank'
+    psMetadata *outhead = psMetadataAlloc();
+    if (phu) {
+        psMetadataCopy (outhead, phu->header);
+    }
+
+    pmConfigConformHeader (outhead, file->format);
+
+    // We need to get some FPA-level concepts in there
+    // This is a hack, not very pretty.  But then, so is writing the FPA in this manner without
+    // using the pmFPAMosaic functions....
+
+    psMetadata *headers = psMetadataLookupMetadata(NULL, file->fpa->camera, BLANK_HEADERS); // Header names
+    if (!headers) {
+        psError(PS_ERR_UNEXPECTED_NULL, false,
+                "Unable to find %s metadata within camera configuration", BLANK_HEADERS);
+        psFree(outhead);
+        return false;
+    }
+
+    {
+        const char *mjdName = psMetadataLookupStr(NULL, headers, "FPA.TIME"); // Header name
+        if (!mjdName || strlen(mjdName) == 0) {
+            psError(PS_ERR_UNEXPECTED_NULL, false,
+                    "Unable to find FPA.TIME in %s within camera configuration.", BLANK_HEADERS);
+            psFree(outhead);
+            return false;
+        }
+        psTime *time = psMetadataLookupTime(NULL, file->fpa->concepts, "FPA.TIME"); // Time of observation
+        double mjd = psTimeToMJD(time); // The MJD of observation
+        psMetadataAddF64(outhead, PS_LIST_TAIL, mjdName, PS_META_REPLACE,
+                         "Time of observation", mjd);
+    }
+
+    {
+        const char *expName = psMetadataLookupStr(NULL, headers, "FPA.EXPOSURE"); // Header name
+        if (!expName || strlen(expName) == 0) {
+            psError(PS_ERR_UNEXPECTED_NULL, false,
+                    "Unable to find FPA.EXPOSURE in %s within camera configuration.",
+                    BLANK_HEADERS);
+            psFree(outhead);
+            return false;
+        }
+        float exptime = psMetadataLookupF32(NULL, file->fpa->concepts,
+                                            "FPA.EXPOSURE"); // Exposure time
+        psMetadataAddF32(outhead, PS_LIST_TAIL, expName, PS_META_REPLACE,
+                         "Exposure time (sec)", exptime);
+    }
+
+    {
+        const char *amName = psMetadataLookupStr(NULL, headers, "FPA.AIRMASS"); // Header name
+        if (!amName || strlen(amName) == 0) {
+            psError(PS_ERR_UNEXPECTED_NULL, false,
+                    "Unable to find FPA.AIRMASS in %s within camera configuration.",
+                    BLANK_HEADERS);
+            psFree(outhead);
+            return false;
+        }
+        float airmass = psMetadataLookupF32(NULL, file->fpa->concepts, "FPA.AIRMASS"); // Airmass
+        psMetadataAddF32(outhead, PS_LIST_TAIL, amName, PS_META_REPLACE,
+                         "Observation airmass", airmass);
+    }
+
+    psMetadata *fileData = psMetadataLookupMetadata(NULL, file->format, "FILE"); // File information
+    const char *fpaNameHdr = psMetadataLookupStr(NULL, fileData, "FPA.NAME");
+    if (fpaNameHdr && strlen(fpaNameHdr) > 0) {
+        const char *fpaName = psMetadataLookupStr(NULL, file->fpa->concepts, "FPA.NAME");
+        psMetadataAddStr(outhead, PS_LIST_TAIL, fpaNameHdr, PS_META_REPLACE, "FPA name", fpaName);
+    }
+
+    // if we have mosaic-level astrometry information, add it here:
+    psMetadata *updates = psMetadataLookupPtr (&status, file->fpa->analysis, "PSASTRO.HEADER");
+    if (updates) {
+        psMetadataCopy (outhead, updates);
+    }
+
+    psMetadataAddBool (outhead, PS_LIST_TAIL, "EXTEND", PS_META_REPLACE, "this file has extensions", true);
+    psFitsWriteBlank (file->fits, outhead, "");
+    file->wrote_phu = true;
+
+    psTrace ("pmFPAfile", 5, "wrote phu %s (type: %d)\n", file->filename, file->type);
+    psFree (outhead);
+
+    return true;
+}
+
+// Given a FITS file pointer, read the table of object data
+// XXX add in error handling
+bool pmFPAviewReadObjects (const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAReadObjects (fpa, view, file, config);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        pmChipReadObjects (chip, view, file, config);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmCellReadObjects (cell, view, file, config);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    pmReadoutReadObjects (readout, view, file, config);
+    return true;
+}
+
+// read in all chip-level Objects files for this FPA
+bool pmFPAReadObjects (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        pmChipReadObjects (chip, thisView, file, config);
+    }
+    psFree (thisView);
+
+    if (!pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_HEADER, true, NULL)) {
+        psError(PS_ERR_IO, false, "Failed to read concepts for fpa.\n");
+        return false;
+    }
+
+    return true;
+}
+
+// read in all cell-level Objects files for this chip
+bool pmChipReadObjects (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    chip->data_exists = false;
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        thisView->cell = i;
+        pmCellReadObjects (cell, thisView, file, config);
+        if (!cell->data_exists) continue;
+        chip->data_exists = true;
+    }
+    psFree (thisView);
+
+    if (!pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_HEADER, true, true, NULL)) {
+        psError(PS_ERR_IO, false, "Failed to read concepts for chip.\n");
+        return false;
+    }
+
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmCellReadObjects (pmCell *cell, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    // multiple readout mode is not yet defined for CMP or CMF files
+    // if they have not been allocated, allocate a single readout
+    if (!cell->readouts || !cell->readouts->n) {
+        pmReadout *readout = pmReadoutAlloc (cell);
+        psFree (readout);
+    }
+
+    cell->data_exists = false;
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        thisView->readout = i;
+        pmReadoutReadObjects (readout, thisView, file, config);
+        if (!readout->data_exists) {
+            continue;
+        }
+
+        // load in the concept information for this cell
+        if (!pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_HEADER, true, NULL)) {
+            //psError(PS_ERR_UNKNOWN, false, "Failed to read concepts for cell");
+            //return false;
+            psWarning("Difficulty reading concepts for cell; attempting to proceed.");
+        }
+        cell->data_exists = true;
+    }
+    psFree (thisView);
+
+    if (!pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_HEADER, true, NULL)) {
+        //psError(PS_ERR_UNKNOWN, false, "Failed to read concepts for cell");
+        //return false;
+        psWarning("Difficulty reading concepts for cell; attempting to proceed.");
+    }
+
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmReadoutReadObjects (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+
+    bool status;
+    psArray *sources = NULL;
+    pmHDU *hdu;
+
+    switch (file->type) {
+      case PM_FPA_FILE_OBJ:
+        psError(PS_ERR_UNKNOWN, true, "OBJ is not supported as an input object format");
+        return false;
+
+      case PM_FPA_FILE_SX:
+        psError(PS_ERR_UNKNOWN, true, "SX is not supported as an input object format");
+        return false;
+
+      case PM_FPA_FILE_CMP:
+        // a SPLIT format : only one header and object table per file
+
+        // read in header, if not yet loaded
+        hdu = pmFPAviewThisHDU (view, file->fpa);
+#if 0
+        if (file->filename)
+            psFree (file->filename);
+        file->filename = pmFPAfileNameFromRule (file->filerule, file, view);
+#endif
+
+        // indirect filenames
+        if (!strcasecmp (file->filename, "@FILES")) {
+            psFree (file->filename);
+            char *filesrc = pmFPAfileNameFromRule (file->filesrc, file, view);
+            file->filename = psMetadataLookupStr (&status, file->names, filesrc);
+            psFree (filesrc);
+            if (file->filename == NULL) return false;
+            // psMetadataLookupStr just returns a view, file->filename must be protected
+            psMemIncrRefCounter (file->filename);
+        }
+
+        // read the PHU from this file
+        file->fits = psFitsOpen (file->filename, "r");
+        if (hdu->header != NULL) {
+            psFree (hdu->header);
+        }
+        hdu->header = psFitsReadHeader (NULL, file->fits);
+        psFitsClose (file->fits);
+        file->fits = NULL;
+
+        sources = pmSourcesReadCMP (file->filename, hdu->header);
+        break;
+
+      case PM_FPA_FILE_CMF:
+        // read in header, if not yet loaded
+        hdu = pmFPAviewThisHDU (view, file->fpa);
+
+        // lookup the EXTNAME values used for table data and image header segments
+        char *rule = NULL;
+        // Menu of EXTNAME rules
+        psMetadata *menu = psMetadataLookupMetadata(&status, file->camera, "EXTNAME.RULES");
+        if (!menu) {
+            psError(PS_ERR_UNKNOWN, true, "missing EXTNAME.RULES in camera.config");
+            return false;
+        }
+        // EXTNAME for image header
+        rule = psMetadataLookupStr(&status, menu, "CMF.HEAD");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, true, "missing entry for CMF.HEAD in EXTNAME.RULES in camera.config");
+            return false;
+        }
+        char *headname = pmFPAfileNameFromRule (rule, file, view);
+        // EXTNAME for table data
+        rule = psMetadataLookupStr(&status, menu, "CMF.DATA");
+        if (!rule) {
+            psError(PS_ERR_UNKNOWN, true, "missing entry for CMF.DATA in EXTNAME.RULES in camera.config");
+            return false;
+        }
+        char *dataname = pmFPAfileNameFromRule (rule, file, view);
+
+        // advance to the IMAGE HEADER extension
+        if (hdu->header == NULL) {
+            // if the IMAGE header does not exist, we have no data for this view
+            if (!psFitsMoveExtName (file->fits, headname)) {
+                readout->data_exists = false;
+                psFree (headname);
+                psFree (dataname);
+                return true;
+            }
+            hdu->header = psFitsReadHeader (NULL, file->fits);
+        }
+
+        // we need to find the corresponding table EXTNAME.
+        // first check the header
+        char *extdata = psMetadataLookupStr (NULL, hdu->header, "EXTDATA");
+        if (extdata) {
+            // if EXTDATA is defined in the header, use that value for 'dataname'
+            psFree (dataname);
+            dataname = psMemIncrRefCounter (extdata);
+        }
+
+        // advance to the table data extension
+        // since we have read the IMAGE header, the TABLE header should exist
+        if (!psFitsMoveExtName (file->fits, dataname)) {
+            psAbort("cannot find data extension %s in %s", dataname, file->filename);
+        }
+
+        psMetadata *tableHeader = psFitsReadHeader(NULL, file->fits); // The FITS header
+        if (!tableHeader) psAbort("cannot read table header");
+
+        char *exttype = psMetadataLookupStr (NULL, tableHeader, "EXTTYPE");
+        if (!exttype) psAbort("cannot read table type");
+
+        // XXX these are case-sensitive since the EXTYPE is case-sensitive
+        if (!strcmp (exttype, "SMPDATA")) {
+            sources = pmSourcesRead_SMPDATA (file->fits, hdu->header);
+        }
+        if (!strcmp (exttype, "PS1_DEV_0")) {
+            sources = pmSourcesRead_PS1_DEV_0 (file->fits, hdu->header);
+        }
+        if (!strcmp (exttype, "PS1_DEV_1")) {
+            sources = pmSourcesRead_PS1_DEV_1 (file->fits, hdu->header);
+        }
+
+        psTrace("psModules.objects", 6, "read CMF table from %s : %s : %s", file->filename, headname, dataname);
+        psFree (headname);
+        psFree (dataname);
+        psFree (tableHeader);
+        break;
+
+      default:
+        fprintf (stderr, "warning: type mismatch\n");
+        break;
+    }
+    readout->data_exists = true;
+    status = psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSPHOT.SOURCES", PS_DATA_ARRAY, "psphot sources", sources);
+    psFree (sources);
+    return true;
+}
+
+bool pmFPAviewCheckDataStatusForSources (const pmFPAview *view, const pmFPAfile *file)
+{
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        bool exists = pmFPACheckDataStatusForSources (fpa);
+        return exists;
+    }
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        bool exists = pmChipCheckDataStatusForSources (chip);
+        return exists;
+    }
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        bool exists = pmCellCheckDataStatusForSources (cell);
+        return exists;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        psError(PS_ERR_IO, true, "Requested readout == %d >= cell->readouds->n == %ld", view->readout, cell->readouts->n);
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    bool exists = pmReadoutCheckDataStatusForSources (readout);
+    return exists;
+}
+
+bool pmFPACheckDataStatusForSources (const pmFPA *fpa) {
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        if (pmChipCheckDataStatusForSources (chip)) return true;
+    }
+    return false;
+}
+
+bool pmChipCheckDataStatusForSources (const pmChip *chip) {
+
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        if (!cell) continue;
+        if (pmCellCheckDataStatusForSources (cell)) return true;
+    }
+    return false;
+}
+
+bool pmCellCheckDataStatusForSources (const pmCell *cell) {
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        if (!readout) continue;
+        if (pmReadoutCheckDataStatusForSources (readout)) return true;
+    }
+    return false;
+}
+
+bool pmReadoutCheckDataStatusForSources (const pmReadout *readout) {
+
+    bool status;
+
+    // select the psf of interest
+    pmPSF *psf = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.SOURCES");
+    if (!psf) return false;
+    return true;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO.h	(revision 15358)
@@ -0,0 +1,63 @@
+/* @file  pmSourceIO.h
+ * @brief functions to read and write object files
+ *
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-09-27 03:35:29 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_SOURCE_IO_H
+# define PM_SOURCE_IO_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+int pmSourceGetDophotType (pmSource *source);
+bool pmSourceSetDophotType (pmSource *source, int type);
+
+bool pmSourcesWriteRAW (psArray *sources, char *filename);
+bool pmSourcesWriteOBJ (psArray *sources, char *filename);
+bool pmSourcesWriteSX (psArray *sources, char *filename);
+bool pmSourcesWriteCMP (psArray *sources, char *filename, psMetadata *header);
+
+bool pmSourcesWrite_SMPDATA (psFits *fits, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname);
+bool pmSourcesWrite_PS1_DEV_0 (psFits *fits, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname);
+bool pmSourcesWrite_PS1_DEV_1 (psFits *fits, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname);
+
+bool pmSource_CMF_WritePHU (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+
+psArray *pmSourcesReadCMP (char *filename, psMetadata *header);
+
+psArray *pmSourcesRead_SMPDATA (psFits *fits, psMetadata *header);
+psArray *pmSourcesRead_PS1_DEV_0 (psFits *fits, psMetadata *header);
+psArray *pmSourcesRead_PS1_DEV_1 (psFits *fits, psMetadata *header);
+
+bool pmSourcesWritePSFs (psArray *sources, char *filename);
+bool pmSourcesWriteEXTs (psArray *sources, char *filename, bool require);
+bool pmSourcesWriteNULLs (psArray *sources, char *filename);
+bool pmMomentsWriteText (psArray *sources, char *filename);
+bool pmPeaksWriteText (psArray *peaks, char *filename);
+
+bool pmFPAviewReadObjects (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmFPAReadObjects (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmChipReadObjects (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmCellReadObjects (pmCell *cell, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmReadoutReadObjects (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+
+bool pmFPAviewWriteObjects (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmFPAWriteObjects (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmChipWriteObjects (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmCellWriteObjects (pmCell *cell, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmReadoutWriteObjects (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+
+bool pmFPAviewCheckDataStatusForSources (const pmFPAview *view, const pmFPAfile *file);
+bool pmFPACheckDataStatusForSources (const pmFPA *fpa);
+bool pmChipCheckDataStatusForSources (const pmChip *chip);
+bool pmCellCheckDataStatusForSources (const pmCell *cell);
+bool pmReadoutCheckDataStatusForSources (const pmReadout *readout);
+
+/// @}
+# endif /* PM_SOURCE_IO_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_CMP.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_CMP.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_CMP.c	(revision 15358)
@@ -0,0 +1,290 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.31 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourceIO.h"
+
+// XXX make sure in and out have consistent zero-point adjustments
+// XXX make sure the angle in correctly translated to/from degrees
+// XXX we lose all information from the 'type' field
+
+// XXX update this file is we convert to PAR[4] : SigmaX*sqrt(2) (not 1/SigmaX)
+
+// elixir-style pseudo FITS table (header + ascii list)
+bool pmSourcesWriteCMP (psArray *sources, char *filename, psMetadata *header)
+{
+
+    int i, type;
+    // psMetadataItem *mdi;
+    psF32 *PAR, *dPAR;
+    float lsky = 0;
+    bool status;
+    psEllipseAxes axes;
+
+    // find config information for output header
+    float ZERO_POINT = psMetadataLookupF32 (&status, header, "ZERO_PT");
+    if (!status)
+        ZERO_POINT = 25.0;
+
+    // MEF elements have XTENSION, not SIMPLE: remove this (replace with SIMPLE)
+    psMetadataLookupStr (&status, header, "XTENSION");
+    if (status)
+        psMetadataRemoveKey (header, "XTENSION");
+
+    // create file, write-out header
+    psMetadataAddS32 (header, PS_LIST_HEAD, "NAXIS", PS_META_REPLACE, "head data only", 0);
+    psMetadataAddBool (header, PS_LIST_HEAD, "SIMPLE", PS_META_REPLACE, "CMP file, not simple", false);
+
+    psFits *fits = psFitsOpen (filename, "w");
+    if (fits == NULL) {
+        psError(PS_ERR_IO, false, "can't open output file for write %s\n", filename);
+        return false;
+    }
+    // XXX what is the EXTNAME??
+    if (!psFitsWriteBlank(fits, header, "")) {
+        psError(PS_ERR_IO, false, "Writing header to %s\n", filename);
+        (void)psFitsClose(fits);
+        return false;
+    }
+    if (!psFitsClose(fits)) {
+        const psErrorCode code = psErrorCodeLast();
+
+        if (code == PS_ERR_BAD_FITS) {
+            psErrorClear();
+        } else {
+            psError(PS_ERR_IO, false, "Closing %s\n", filename);
+            return false;
+        }
+    }
+
+    // re-open, add data to end of file
+    FILE *f = fopen (filename, "a+");
+    if (f == NULL) {
+        psLogMsg ("WriteSourceOBJ", 3, "can't reopen output file for append %s\n", filename);
+        psError(PS_ERR_IO, false, "can't open output file for output %s\n", filename);
+        return false;
+    }
+
+    fseeko(f, 0, SEEK_END);
+
+    psLine *line = psLineAlloc (67);  // 66 is imclean-defined line length
+
+    // write sources with models first
+    for (i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+
+	// no difference between PSF and non-PSF model
+        pmModel *model = pmSourceGetModel (NULL, source);
+        if (model == NULL)
+            continue;
+
+        PAR = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        type = pmSourceGetDophotType (source);
+        lsky = (source->sky < 1.0) ? 0.0 : log10(source->sky);
+
+        axes = pmPSF_ModelToAxes (PAR, 20.0);
+
+        float errMag = isfinite(source->errMag) ? source->errMag : 999;
+
+        psLineInit (line);
+        psLineAdd (line, "%6.1f ",  PAR[PM_PAR_XPOS]);
+        psLineAdd (line, "%6.1f ",  PAR[PM_PAR_YPOS]);
+        psLineAdd (line, "%6.3f ",  PS_MIN (99.0, source->psfMag + ZERO_POINT));
+        psLineAdd (line, "%03d ",   PS_MIN (999, (int)errMag));
+        psLineAdd (line, "%2d ",    type);
+        psLineAdd (line, "%3.1f ",  lsky);
+        psLineAdd (line, "%6.3f ",  PS_MIN (99.0, source->extMag + ZERO_POINT));
+        psLineAdd (line, "%6.3f ",  PS_MIN (99.0, source->apMag  + ZERO_POINT));
+        psLineAdd (line, "%6.2f ",  axes.major);
+        psLineAdd (line, "%6.2f ",  axes.minor);
+        psLineAdd (line, "%5.1f\n", axes.theta);
+        fwrite (line->line, 1, line->Nline, f);
+    }
+    fclose (f);
+    psFree (line);
+    return true;
+}
+
+# define BYTES_STAR 66
+# define BLOCK 1000
+
+// elixir-style pseudo FITS table (header + ascii list)
+psArray *pmSourcesReadCMP (char *filename, psMetadata *header)
+{
+
+    bool status;
+    int Ninstar;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+
+    // define PSF model type
+    int modelType = pmModelClassGetType ("PS_MODEL_GAUSS");
+
+    char *PSF_NAME = psMetadataLookupStr (&status, header, "PSF_NAME");
+    if (PSF_NAME != NULL) {
+        modelType = pmModelClassGetType (PSF_NAME);
+    }
+
+    // find config information for output header
+    float ZERO_POINT = psMetadataLookupF32 (&status, header, "ZERO_PT");
+    if (!status)
+        ZERO_POINT = 25.0;
+
+    // how many lines in the header?
+    long nLines = header->list->n;
+    off_t nBytes = nLines * 80;
+    if (nBytes % 2880) {
+        off_t nBlock = 1 + (off_t)(nBytes / 2880);
+        nBytes = nBlock * 2880;
+    }
+
+    // re-open, seek to end of header
+    FILE *f = fopen (filename, "r");
+    if (f == NULL) {
+        psLogMsg ("pmSourcesReadCMP", 3, "can't open output file for input %s\n", filename);
+        return NULL;
+    }
+
+    fseeko(f, nBytes, SEEK_SET);
+
+    // prepare array to store data
+    int nStars = psMetadataLookupS32 (&status, header, "NSTARS");
+    psArray *sources = psArrayAlloc (nStars);
+    sources->n = 0;
+
+    // we have fixed bytes / line : use that info
+    // XXX use the min of nStars and BLOCK?
+    char *buffer = psAlloc (BYTES_STAR*PS_MIN(nStars, BLOCK));
+
+    int Nextra = 0;
+    while (true) {
+        /* load next data block */
+        // XXX fix the use of two vars with different case -JH
+        off_t Nbytes = BYTES_STAR * BLOCK - Nextra;
+        off_t nbytes = fread (&buffer[Nextra], 1, Nbytes, f);
+        if (nbytes == 0) {
+            goto done_load;
+        }
+        nbytes += Nextra;
+
+        /* check line-by-line integrity */
+        char *c  = buffer;
+        char *c2 = NULL;
+        while (c < buffer + nbytes) {
+            for (c2 = c; *c2 == '\n'; c2++)
+                ;
+            if (c2 > c) { /* extra return chars */
+                memmove (c, c2, (int)(buffer + nbytes - c2));
+                int Nskip = c2 - c;
+                nbytes -= Nskip;
+                memset(buffer + nbytes, '\0', Nskip);
+                psLogMsg (__func__, 4, "deleted %d extra return chars\n", Nskip);
+            }
+            c2 = strchr (c, '\n');
+            if (c2 == (char *) NULL) {
+                goto done_check;
+            }
+            c2++;
+            if ((c2 - c) != BYTES_STAR) { /* bad line, delete it */
+                memmove (c, c2, (int)(buffer + nbytes - c2));
+                int Nskip = c2 - c;
+                nbytes -= Nskip;
+		memset(buffer + nbytes, '\0', Nskip);
+                psLogMsg (__func__, 4, "deleted line, %d extra chars\n", Nskip);
+            } else {
+                c = c2;
+            }
+        }
+done_check:
+
+        /* extract data for stars */
+        Ninstar = nbytes / BYTES_STAR;
+        Nextra = nbytes % BYTES_STAR;
+        for (int j = 0; j < Ninstar; j++) {
+            psString line = psStringNCopy (&buffer[j*BYTES_STAR], BYTES_STAR);
+
+            psArray *array = psStringSplitArray (line, " ", false);
+
+            // XXX this is a bit cheap: I don't even attempt to interpret the
+            // sextractor / dophot analysis to distinguish stars and galaxies
+            // your milage may vary...
+            pmSource *source = pmSourceAlloc ();
+            source->modelPSF = pmModelAlloc (modelType);
+            source->type = PM_SOURCE_TYPE_STAR;
+
+            PAR = source->modelPSF->params->data.F32;
+            dPAR = source->modelPSF->dparams->data.F32;
+
+            PAR[PM_PAR_SKY] = pow (atof (array->data[5]), 10.0);
+            PAR[PM_PAR_XPOS] = atof (array->data[0]);
+            PAR[PM_PAR_YPOS] = atof (array->data[1]);
+            source->psfMag = atof (array->data[2]);
+            source->extMag = atof (array->data[6]);
+            source->errMag = atof (array->data[3]) / 1000.0;
+            source->apMag  = atof (array->data[7]);
+            axes.major     = atof (array->data[8]);
+            axes.minor     = atof (array->data[9]);
+            axes.theta  = atof (array->data[10]);
+
+            if (!isfinite(axes.major))
+                goto skip_source;
+            if (!isfinite(axes.minor))
+                goto skip_source;
+            if (!isfinite(axes.theta))
+                goto skip_source;
+
+	    pmPSF_AxesToModel (PAR, axes);
+
+            psArrayAdd (sources, 100, source);
+
+skip_source:
+            psFree (line);
+            psFree (array);
+            psFree (source);
+
+        }
+    }
+done_load:
+
+    // XXX if sources->n != nStars, give an error?
+    psFree (buffer);
+
+    fclose (f);
+    return (sources);
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_OBJ.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_OBJ.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_OBJ.c	(revision 15358)
@@ -0,0 +1,101 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourceIO.h"
+
+// dophot-style output list with fixed line width
+bool pmSourcesWriteOBJ (psArray *sources, char *filename)
+{
+
+    int type;
+    psF32 *PAR, *dPAR;
+    float dmag, apResid;
+    psEllipseAxes axes;
+
+    psTimerStart ("string");
+
+    psLine *line = psLineAlloc (104);  // 104 is dophot-defined line length
+
+    FILE *f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg (__func__, 3, "can't open output file for output %s\n", filename);
+        return false;
+    }
+
+    // write sources with models
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+
+	// no difference between PSF and non-PSF model
+        pmModel *model = pmSourceGetModel (NULL, source);
+        if (model == NULL)
+            continue;
+
+        PAR = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        dmag = dPAR[PM_PAR_I0] / PAR[PM_PAR_I0];
+        type = pmSourceGetDophotType (source);
+        if ((source->apMag < 99.0) && (source->psfMag < 99.0)) {
+            apResid = source->apMag - source->psfMag;
+        } else {
+            apResid = 0.0;
+        }
+
+        axes = pmPSF_ModelToAxes (PAR, 20.0);
+
+        psLineInit (line);
+        psLineAdd (line, "%3d",   type);
+        psLineAdd (line, "%8.2f", PAR[PM_PAR_XPOS]);
+        psLineAdd (line, "%8.2f", PAR[PM_PAR_YPOS]);
+        psLineAdd (line, "%8.3f", source->psfMag);
+        psLineAdd (line, "%6.3f", dmag);
+        psLineAdd (line, "%9.2f", source->sky);
+        psLineAdd (line, "%9.3f", axes.major);
+        psLineAdd (line, "%9.3f", axes.minor);
+        psLineAdd (line, "%7.2f", axes.theta);
+        psLineAdd (line, "%8.3f", source->extMag);
+        psLineAdd (line, "%8.3f", source->apMag);
+        psLineAdd (line, "%8.2f\n", apResid);
+        fwrite (line->line, 1, line->Nline, f);
+    }
+    fclose (f);
+    psFree (line);
+    fprintf (stderr, "%f seconds for %d objects\n", psTimerMark ("string"), (int)sources->n);
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_PS1_DEV_0.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_PS1_DEV_0.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_PS1_DEV_0.c	(revision 15358)
@@ -0,0 +1,216 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourceIO.h"
+
+// panstars-style FITS table output (header + table in 1st extension)
+// this format consists of a header derived from the image header
+// followed by a zero-size matrix, followed by the table data
+
+// this output format is valid for psphot analysis of an image, and does not include calibrated
+// values derived in the DVO database.
+// XXX how do I generate the source tables which I need to send to PSPS?
+
+bool pmSourcesWrite_PS1_DEV_0 (psFits *fits, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname)
+{
+
+    psArray *table;
+    psMetadata *row;
+    int i;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+    psF32 xPos, yPos;
+    psF32 xErr, yErr;
+
+    table = psArrayAllocEmpty (sources->n);
+
+    // we write out all sources, regardless of quality.  the source flags tell us the state
+    for (i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+
+        // no difference between PSF and non-PSF model
+        pmModel *model = pmSourceGetModel (NULL, source);
+
+        if (model != NULL) {
+            PAR = model->params->data.F32;
+            dPAR = model->dparams->data.F32;
+            xPos = PAR[PM_PAR_XPOS];
+            yPos = PAR[PM_PAR_YPOS];
+            xErr = dPAR[PM_PAR_XPOS];
+            yErr = dPAR[PM_PAR_YPOS];
+
+            axes = pmPSF_ModelToAxes (PAR, 20.0);
+        } else {
+            xPos = source->peak->xf;
+            yPos = source->peak->yf;
+            xErr = 0.0; // XXX a better choice, please
+            yErr = 0.0; // XXX a better choice, please
+            axes.major = 0.0;
+            axes.minor = 0.0;
+            axes.theta = 0.0;
+        }
+
+        float peakMag = (source->peak->flux > 0) ? -2.5*log10(source->peak->flux) : NAN;
+        psS16 nImageOverlap = 1;
+        psS32 ID = 0; // XXX need to figure out how to generate this
+
+        row = psMetadataAlloc ();
+        // XXX we are not writing out the mode (flags) or the type (psf, ext, etc)
+        psMetadataAdd (row, PS_LIST_TAIL, "IPP_IDET",         PS_DATA_U32, "IPP detection identifier index",             ID);
+        psMetadataAdd (row, PS_LIST_TAIL, "X_PSF",            PS_DATA_F32, "PSF x coordinate",                           xPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_PSF",            PS_DATA_F32, "PSF y coordinate",                           yPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "X_PSF_SIG",        PS_DATA_F32, "Sigma in PSF x coordinate",                  xErr);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_PSF_SIG",        PS_DATA_F32, "Sigma in PSF y coordinate",                  yErr);
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_INST_MAG",     PS_DATA_F32, "PSF fit instrumental magnitude",             PS_MIN (99.0, source->psfMag));
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_INST_MAG_SIG", PS_DATA_F32, "Sigma of PSF instrumental magnitude",        PS_MIN (99.0, source->errMag));
+        psMetadataAdd (row, PS_LIST_TAIL, "PEAK_FLUX_AS_MAG", PS_DATA_F32, "Peak flux expressed as magnitude",           PS_MIN (99.0, peakMag));
+        psMetadataAdd (row, PS_LIST_TAIL, "SKY",              PS_DATA_F32, "Sky level",                                  source->sky);
+        psMetadataAdd (row, PS_LIST_TAIL, "SKY_SIGMA",        PS_DATA_F32, "Sigma of sky level",                         source->skyErr);
+        // XXX this is called STAR_GALAXY_SEP in the ICD; PSF_PROB is better
+        // XXX need to set this value in psphotEvalPSF
+        // XXX can I use the 2d polynomial peak fit to constrain this for the low-sn sources?
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_PROBABILITY",  PS_DATA_F32,  "Probability of PSF-ness",                   NAN);
+        // XXX these should be major and minor, not 'x' and 'y'
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_WIDTH_X",      PS_DATA_F32, "PSF width in x coordinate",                  axes.major);
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_WIDTH_Y",      PS_DATA_F32, "PSF width in y coordinate",                  axes.minor);
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_THETA",        PS_DATA_F32, "PSF orientation angle",                      axes.theta);
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_QF",           PS_DATA_F32, "PSF coverage/quality factor",                source->pixWeight);
+        // XXX not sure how to get this : need to load Nimages with weight
+        psMetadataAdd (row, PS_LIST_TAIL, "N_FRAMES",         PS_DATA_U16, "Number of frames overlapping source center", nImageOverlap);
+        psMetadataAdd (row, PS_LIST_TAIL, "DUMMY",            PS_DATA_U16, "padding", 0);
+
+        // XXX these calibrated values are not supplied by psphot analysis
+        // psMetadataAdd (row, PS_LIST_TAIL, "RA_PSF",           PS_DATA_F64, "RA from PSF fit",                         RA);
+        // psMetadataAdd (row, PS_LIST_TAIL, "DEC_PSF",          PS_DATA_F64, "DEC from PSF fit",                        DEC);
+        // psMetadataAdd (row, PS_LIST_TAIL, "RA_PSF_SIG",       PS_DATA_F32, "Sigma of PSF fit RA",                     dRA);
+        // psMetadataAdd (row, PS_LIST_TAIL, "DEC_PSF_SIG",      PS_DATA_F32, "Sigma of PSF fit DEC",                    dDEC);
+        // psMetadataAdd (row, PS_LIST_TAIL, "CAL_PSF_MAG",      PS_DATA_F32, "Calibrated magnitude",                    calMag);
+        // psMetadataAdd (row, PS_LIST_TAIL, "CAL_PSF_MAG_sIG",  PS_DATA_F32, "Sigma of calibrated magnitude",           calMagErr);
+
+        psArrayAdd (table, 100, row);
+        psFree (row);
+    }
+
+    if (table->n == 0) {
+        psFitsWriteBlank (fits, tableHeader, extname);
+        psFree (table);
+        return true;
+    }
+
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTable (fits, tableHeader, table, extname)) {
+        psError(PS_ERR_IO, false, "writing ext data %s\n", extname);
+        psFree(table);
+        return false;
+    }
+
+    psFree (table);
+    return true;
+}
+
+// read in a readout from the fits file
+psArray *pmSourcesRead_PS1_DEV_0 (psFits *fits, psMetadata *header)
+{
+
+    bool status;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+
+    // define PSF model type
+    int modelType = pmModelClassGetType ("PS_MODEL_GAUSS");
+
+    char *PSF_NAME = psMetadataLookupStr (&status, header, "PSF_NAME");
+    if (PSF_NAME != NULL) {
+        modelType = pmModelClassGetType (PSF_NAME);
+    }
+
+    psArray *table = psFitsReadTable (fits);
+    // validate a single row of the table (must match SMP)
+
+    psArray *sources = psArrayAlloc (table->n);
+
+    // convert the table to the pmSource entries
+    // XXX need to chooose PSF vs EXT, based on type?
+    for (int i = 0; i < table->n; i++) {
+        pmSource *source = pmSourceAlloc ();
+        pmModel *model = pmModelAlloc (modelType);
+        source->modelPSF  = model;
+        source->type = PM_SOURCE_TYPE_STAR;
+
+        // NOTE: A SEGV here because "model" is NULL is probably caused by not initialising the models.
+        PAR = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        psMetadata *row = table->data[i];
+
+        PAR[PM_PAR_XPOS]  = psMetadataLookupF32 (&status, row, "X_PSF");
+        PAR[PM_PAR_YPOS]  = psMetadataLookupF32 (&status, row, "Y_PSF");
+        dPAR[PM_PAR_XPOS] = psMetadataLookupF32 (&status, row, "X_PSF_SIG");
+        dPAR[PM_PAR_YPOS] = psMetadataLookupF32 (&status, row, "Y_PSF_SIG");
+        axes.major        = psMetadataLookupF32 (&status, row, "PSF_WIDTH_X");
+        axes.minor        = psMetadataLookupF32 (&status, row, "PSF_WIDTH_Y");
+        axes.theta        = psMetadataLookupF32 (&status, row, "PSF_THETA");
+
+        PAR[PM_PAR_SKY]   = psMetadataLookupF32 (&status, row, "SKY");
+        dPAR[PM_PAR_SKY]  = psMetadataLookupF32 (&status, row, "SKY_SIGMA");
+        source->sky = PAR[PM_PAR_SKY];
+        source->skyErr = dPAR[PM_PAR_SKY];
+
+        // XXX use these to determine PAR[PM_PAR_I0]?
+        source->psfMag    = psMetadataLookupF32 (&status, row, "PSF_INST_MAG");
+        source->errMag    = psMetadataLookupF32 (&status, row, "PSF_INST_MAG_SIG");
+
+        pmPSF_AxesToModel (PAR, axes);
+
+        float lflux = psMetadataLookupF32 (&status, row, "PEAK_FLUX_AS_MAG");
+        float flux = (isfinite(lflux)) ? pow(10.0, -0.4*lflux) : NAN;
+        source->peak = pmPeakAlloc(PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], flux, PM_PEAK_LONE);
+        source->peak->flux = flux;
+
+        source->pixWeight = psMetadataLookupF32 (&status, row, "PSF_QF");
+
+        // XXX other values saved but not loaded?
+        // psMetadataLookupS64 (&status, row, "IPP_IDET");
+        // psMetadataLookupF32 (&status, row, "PSF_PROBABILITY");
+        // psMetadataLookupF32 (&status, row, "N_FRAMES");
+
+        sources->data[i] = source;
+    }
+    psFree (table);
+    return (sources);
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_PS1_DEV_1.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_PS1_DEV_1.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_PS1_DEV_1.c	(revision 15358)
@@ -0,0 +1,242 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-10-09 03:10:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourceIO.h"
+
+// panstarrs-style FITS table output (header + table in 1st extension)
+// this format consists of a header derived from the image header
+// followed by a zero-size matrix, followed by the table data
+
+// this output format is valid for psphot analysis of an image, and does not include calibrated
+// values derived in the DVO database.
+// XXX how do I generate the source tables which I need to send to PSPS?
+
+bool pmSourcesWrite_PS1_DEV_1 (psFits *fits, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname)
+{
+
+    psArray *table;
+    psMetadata *row;
+    int i;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+    psF32 xPos, yPos;
+    psF32 xErr, yErr;
+
+    // let's write these out in S/N order
+    sources = psArraySort (sources, pmSourceSortBySN);
+
+    table = psArrayAllocEmpty (sources->n);
+
+    // we write out all sources, regardless of quality.  the source flags tell us the state
+    for (i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+        source->seq = i;
+
+        // no difference between PSF and non-PSF model
+        pmModel *model = pmSourceGetModel (NULL, source);
+
+        if (model != NULL) {
+            PAR = model->params->data.F32;
+            dPAR = model->dparams->data.F32;
+            xPos = PAR[PM_PAR_XPOS];
+            yPos = PAR[PM_PAR_YPOS];
+            xErr = dPAR[PM_PAR_XPOS];
+            yErr = dPAR[PM_PAR_YPOS];
+
+            axes = pmPSF_ModelToAxes (PAR, 20.0);
+        } else {
+            xPos = source->peak->xf;
+            yPos = source->peak->yf;
+            xErr = source->peak->dx;
+            yErr = source->peak->dy;
+            axes.major = 0.0;
+            axes.minor = 0.0;
+            axes.theta = 0.0;
+        }
+
+        float peakMag = (source->peak->flux > 0) ? -2.5*log10(source->peak->flux) : NAN;
+        psS16 nImageOverlap = 1;
+
+        row = psMetadataAlloc ();
+        // XXX we are not writing out the mode (flags) or the type (psf, ext, etc)
+        psMetadataAdd (row, PS_LIST_TAIL, "IPP_IDET",         PS_DATA_U32, "IPP detection identifier index",             source->seq);
+        psMetadataAdd (row, PS_LIST_TAIL, "X_PSF",            PS_DATA_F32, "PSF x coordinate",                           xPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_PSF",            PS_DATA_F32, "PSF y coordinate",                           yPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "X_PSF_SIG",        PS_DATA_F32, "Sigma in PSF x coordinate",                  xErr);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_PSF_SIG",        PS_DATA_F32, "Sigma in PSF y coordinate",                  yErr);
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_INST_MAG",     PS_DATA_F32, "PSF fit instrumental magnitude",             PS_MIN (99.0, source->psfMag));
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_INST_MAG_SIG", PS_DATA_F32, "Sigma of PSF instrumental magnitude",        PS_MIN (99.0, source->errMag));
+        psMetadataAdd (row, PS_LIST_TAIL, "PEAK_FLUX_AS_MAG", PS_DATA_F32, "Peak flux expressed as magnitude",           PS_MIN (99.0, peakMag));
+        psMetadataAdd (row, PS_LIST_TAIL, "SKY",              PS_DATA_F32, "Sky level",                                  source->sky);
+        psMetadataAdd (row, PS_LIST_TAIL, "SKY_SIGMA",        PS_DATA_F32, "Sigma of sky level",                         source->skyErr);
+
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_PROBABILITY",  PS_DATA_F32,  "Probability of PSF-ness",                   source->psfProb);
+        psMetadataAdd (row, PS_LIST_TAIL, "CR_NSIGMA",        PS_DATA_F32,  "Nsigma deviations from PSF to CF",          source->crNsigma);
+        psMetadataAdd (row, PS_LIST_TAIL, "EXT_NSIGMA",       PS_DATA_F32,  "Nsigma deviations from PSF to EXT",         source->extNsigma);
+
+        // XXX these should be major and minor, not 'x' and 'y'
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_WIDTH_X",      PS_DATA_F32, "PSF width in x coordinate",                  axes.major);
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_WIDTH_Y",      PS_DATA_F32, "PSF width in y coordinate",                  axes.minor);
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_THETA",        PS_DATA_F32, "PSF orientation angle",                      axes.theta);
+        psMetadataAdd (row, PS_LIST_TAIL, "PSF_QF",           PS_DATA_F32, "PSF coverage/quality factor",                source->pixWeight);
+
+        // XXX not sure how to get this : need to load Nimages with weight?
+        psMetadataAdd (row, PS_LIST_TAIL, "N_FRAMES",         PS_DATA_U16, "Number of frames overlapping source center", nImageOverlap);
+        psMetadataAdd (row, PS_LIST_TAIL, "FLAGS",            PS_DATA_U16, "psphot analysis flags",                      source->mode);
+
+        // XXX these calibrated values are not supplied by psphot analysis
+        // psMetadataAdd (row, PS_LIST_TAIL, "RA_PSF",           PS_DATA_F64, "RA from PSF fit",                         RA);
+        // psMetadataAdd (row, PS_LIST_TAIL, "DEC_PSF",          PS_DATA_F64, "DEC from PSF fit",                        DEC);
+        // psMetadataAdd (row, PS_LIST_TAIL, "RA_PSF_SIG",       PS_DATA_F32, "Sigma of PSF fit RA",                     dRA);
+        // psMetadataAdd (row, PS_LIST_TAIL, "DEC_PSF_SIG",      PS_DATA_F32, "Sigma of PSF fit DEC",                    dDEC);
+        // psMetadataAdd (row, PS_LIST_TAIL, "CAL_PSF_MAG",      PS_DATA_F32, "Calibrated magnitude",                    calMag);
+        // psMetadataAdd (row, PS_LIST_TAIL, "CAL_PSF_MAG_sIG",  PS_DATA_F32, "Sigma of calibrated magnitude",           calMagErr);
+
+        psArrayAdd (table, 100, row);
+        psFree (row);
+    }
+
+    if (table->n == 0) {
+        psFitsWriteBlank (fits, tableHeader, extname);
+        psFree (table);
+        return true;
+    }
+
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTable (fits, tableHeader, table, extname)) {
+        psError(PS_ERR_IO, false, "writing ext data %s\n", extname);
+        psFree(table);
+        return false;
+    }
+
+    psFree (table);
+    return true;
+}
+
+// read in a readout from the fits file
+psArray *pmSourcesRead_PS1_DEV_1 (psFits *fits, psMetadata *header)
+{
+
+    bool status;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+
+    // define PSF model type
+    int modelType = pmModelClassGetType ("PS_MODEL_GAUSS");
+
+    char *PSF_NAME = psMetadataLookupStr (&status, header, "PSF_NAME");
+    if (PSF_NAME != NULL) {
+        modelType = pmModelClassGetType (PSF_NAME);
+    }
+    assert (modelType > -1);
+
+    // validate a single row of the table (must match SMP)
+
+    // XXX test return values
+
+    // XXX we have a memory problem, which is illustrated here: if I allocate the sources array
+    // (line 1) before freeing the table, the data is not really freed (but not a leak?)  if I
+    // allocate the sources array *after* freeing the table, the data is actually freed
+    // psArray *fooSources = psArrayAllocEmpty (10000);
+    // psFree (table);
+    // return (fooSources);
+
+
+    // We get the size of the table, and allocate the array of sources first because the table is large and
+    // ephemeral --- when the table gets blown away, whatever is allocated after the table is read.  In fact,
+    // it's better to read the table row by row.
+    long numSources = psFitsTableSize(fits); // Number of sources in table
+    psArray *sources = psArrayAlloc(numSources); // Array of sources, to return
+
+    // convert the table to the pmSource entries
+    // XXX need to chooose PSF vs EXT, based on type?
+    for (int i = 0; i < numSources; i++) {
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+
+        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.
+        PAR = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        PAR[PM_PAR_XPOS]  = psMetadataLookupF32 (&status, row, "X_PSF");
+        PAR[PM_PAR_YPOS]  = psMetadataLookupF32 (&status, row, "Y_PSF");
+        dPAR[PM_PAR_XPOS] = psMetadataLookupF32 (&status, row, "X_PSF_SIG");
+        dPAR[PM_PAR_YPOS] = psMetadataLookupF32 (&status, row, "Y_PSF_SIG");
+        axes.major        = psMetadataLookupF32 (&status, row, "PSF_WIDTH_X");
+        axes.minor        = psMetadataLookupF32 (&status, row, "PSF_WIDTH_Y");
+        axes.theta        = psMetadataLookupF32 (&status, row, "PSF_THETA");
+
+        PAR[PM_PAR_SKY]   = psMetadataLookupF32 (&status, row, "SKY");
+        dPAR[PM_PAR_SKY]  = psMetadataLookupF32 (&status, row, "SKY_SIGMA");
+        source->sky = PAR[PM_PAR_SKY];
+        source->skyErr = dPAR[PM_PAR_SKY];
+
+        // XXX use these to determine PAR[PM_PAR_I0]?
+        source->psfMag    = psMetadataLookupF32 (&status, row, "PSF_INST_MAG");
+        source->errMag    = psMetadataLookupF32 (&status, row, "PSF_INST_MAG_SIG");
+
+        pmPSF_AxesToModel (PAR, axes);
+
+        float lflux = psMetadataLookupF32 (&status, row, "PEAK_FLUX_AS_MAG");
+        float flux = (isfinite(lflux)) ? pow(10.0, -0.4*lflux) : NAN;
+        source->peak = pmPeakAlloc(PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], flux, PM_PEAK_LONE);
+        source->peak->flux = flux;
+
+        source->pixWeight = psMetadataLookupF32 (&status, row, "PSF_QF");
+
+        source->psfProb   = psMetadataLookupF32 (&status, row, "PSF_PROBABILITY");
+        source->crNsigma  = psMetadataLookupF32 (&status, row, "CR_NSIGMA");
+        source->extNsigma = psMetadataLookupF32 (&status, row, "EXT_NSIGMA");
+
+        source->mode      = psMetadataLookupU16 (&status, row, "FLAGS");
+        assert (status);
+
+        // XXX other values saved but not loaded?
+        // psMetadataLookupS64 (&status, row, "IPP_IDET");
+        // psMetadataLookupF32 (&status, row, "N_FRAMES");
+
+        sources->data[i] = source;
+        psFree(row);
+    }
+
+    return sources;
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_RAW.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_RAW.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_RAW.c	(revision 15358)
@@ -0,0 +1,282 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.17 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourcePhotometry.h"
+#include "pmSourceIO.h"
+
+/***** Text Output Methods *****/
+bool pmSourcesWriteRAW (psArray *sources, char *filename)
+{
+
+    char *name = (char *) psAlloc (strlen(filename) + 10);
+
+    sprintf (name, "%s.psf.dat", filename);
+    pmSourcesWritePSFs (sources, name);
+
+    sprintf (name, "%s.ext.dat", filename);
+    pmSourcesWriteEXTs (sources, name, true);
+
+    sprintf (name, "%s.nul.dat", filename);
+    pmSourcesWriteNULLs (sources, name);
+
+    sprintf (name, "%s.mnt.dat", filename);
+    pmMomentsWriteText (sources, name);
+
+    psFree (name);
+    return true;
+}
+
+// write the PSF sources to an output file
+bool pmSourcesWritePSFs (psArray *sources, char *filename)
+{
+
+    double dPos;
+    int i, j;
+    FILE *f;
+    psF32 *PAR, *dPAR;
+    pmModel  *model;
+
+    f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg (__func__, 3, "can't open output file for PSF sources: %s\n", filename);
+        return false;
+    }
+
+    // write sources with models first
+    for (i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+        if (source->type != PM_SOURCE_TYPE_STAR)
+            continue;
+        model = source->modelPSF;
+        if (model == NULL)
+            continue;
+
+        PAR  = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        // dPos is positional error, dMag is mag error
+        dPos = hypot (dPAR[PM_PAR_XPOS], dPAR[PM_PAR_YPOS]);
+
+        fprintf (f, "%7.1f %7.1f  %7.1f %8.4f  %7.4f %7.4f  ",
+                 PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], source->sky,
+                 source->psfMag, source->errMag, dPos);
+
+        for (j = 4; j < model->params->n; j++) {
+            fprintf (f, "%9.6f ", PAR[j]);
+        }
+        fprintf (f, " : ");
+        for (j = 4; j < model->params->n; j++) {
+            fprintf (f, "%9.6f ", dPAR[j]);
+        }
+
+        float logChi = ((model[0].chisq == 0.0) || (model[0].nDOF == 0)) ? NAN : log10(model[0].chisq/model[0].nDOF);
+        float logChiNorm = ((model[0].chisqNorm == 0.0) || (model[0].nDOF == 0)) ? NAN : log10(model[0].chisqNorm/model[0].nDOF);
+
+        fprintf (f, ": %8.4f %2d %#5x %7.3f %7.3f  %7.1f %7.2f %4.2f %4d %2d\n",
+                 source[0].apMag, source[0].type, source[0].mode,
+                 logChi, logChiNorm,
+                 source[0].peak->SN,
+                 model[0].radiusFit,
+                 source[0].pixWeight,
+                 model[0].nDOF,
+                 model[0].nIter);
+    }
+    fclose (f);
+    return true;
+}
+
+// dump the sources to an output file
+bool pmSourcesWriteEXTs (psArray *sources, char *filename, bool requireEXT)
+{
+
+    double dPos;
+    int i, j;
+    FILE *f;
+    psF32 *PAR, *dPAR;
+    pmModel  *model;
+
+    f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg ("pmModelWriteEXTs", 3, "can't open output file for EXT sources: %s\n", filename);
+        return false;
+    }
+
+    // write sources with models first
+    for (i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+
+        if (requireEXT && (source->type != PM_SOURCE_TYPE_EXTENDED))
+            continue;
+
+        model = source->modelEXT;
+        if (model == NULL)
+            continue;
+
+        PAR  = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        // dPos is shape error
+        // XXX these are hardwired for SGAUSS
+        dPos = hypot ((dPAR[PM_PAR_SXX] / PAR[PM_PAR_SXX]), (dPAR[PM_PAR_SYY] / PAR[PM_PAR_SYY]));
+
+        fprintf (f, "%7.1f %7.1f  %7.1f %8.4f  %7.4f %7.4f  ",
+                 PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], source->sky,
+                 source->extMag, source->errMag, dPos);
+
+        for (j = 4; j < model->params->n; j++) {
+            fprintf (f, "%9.6f ", PAR[j]);
+        }
+        fprintf (f, " : ");
+        for (j = 4; j < model->params->n; j++) {
+            fprintf (f, "%9.6f ", dPAR[j]);
+        }
+        fprintf (f, ": %7.4f  %2d %#5x %7.3f %7.3f  %7.1f %7.2f %4.2f %4d %2d\n",
+                 source->apMag,
+                 source[0].type, source[0].mode,
+                 log10(model[0].chisq/model[0].nDOF),
+                 log10(model[0].chisqNorm/model[0].nDOF),
+                 source[0].peak->SN,
+                 model[0].radiusFit,
+                 source[0].pixWeight,
+                 model[0].nDOF,
+                 model[0].nIter);
+    }
+    fclose (f);
+    return true;
+}
+
+// dump the sources to an output file
+bool pmSourcesWriteNULLs (psArray *sources, char *filename)
+{
+
+    int i;
+    FILE *f;
+    pmMoments *moment = NULL;
+    pmSource *source = NULL;
+
+    f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg ("DumpObjects", 3, "can't open output file for NULL sources: %s\n", filename);
+        return false;
+    }
+
+    pmMoments *empty = pmMomentsAlloc ();
+
+    // write sources with models first
+    for (i = 0; i < sources->n; i++) {
+        source = sources->data[i];
+
+        // skip these sources (in PSF or EXT)
+        if (source->type == PM_SOURCE_TYPE_STAR)
+            continue;
+        if (source->type == PM_SOURCE_TYPE_EXTENDED)
+            continue;
+
+        if (source->moments == NULL) {
+            moment = empty;
+        } else {
+            moment = source->moments;
+        }
+
+        fprintf (f, "%5d %5d  %7.1f  %7.1f %7.1f  %6.3f %6.3f  %8.1f %7.1f %7.1f %7.1f  %4d %2d\n",
+                 source->peak->x, source->peak->y, source->peak->value,
+                 moment->x, moment->y,
+                 moment->Sx, moment->Sy,
+                 moment->Sum, moment->Peak,
+                 moment->Sky, moment->SN,
+                 moment->nPixels, source->type);
+    }
+    fclose (f);
+    psFree (empty);
+    return true;
+}
+
+// write the moments to an output file
+bool pmMomentsWriteText (psArray *sources, char *filename)
+{
+
+    int i;
+    FILE *f;
+    pmSource *source = NULL;
+
+    f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg ("pmMomentsWriteText", 3, "can't open output file for moments: %s\n", filename);
+        return false;
+    }
+
+    for (i = 0; i < sources->n; i++) {
+        source = sources->data[i];
+        if (source->moments == NULL)
+            continue;
+        fprintf (f, "%5d %5d  %7.1f  %7.1f %7.1f  %6.3f %6.3f  %10.1f %7.1f %7.1f %7.1f  %4d %2d %#5x\n",
+                 source->peak->x, source->peak->y, source->peak->value,
+                 source->moments->x, source->moments->y,
+                 source->moments->Sx, source->moments->Sy,
+                 source->moments->Sum, source->moments->Peak,
+                 source->moments->Sky, source->moments->SN,
+                 source->moments->nPixels, source->type, source->mode);
+    }
+    fclose (f);
+    return true;
+}
+
+// write the peaks to an output file
+bool pmPeaksWriteText (psArray *peaks, char *filename)
+{
+
+    int i;
+    FILE *f;
+
+    f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg ("pmPeaksWriteText", 3, "can't open output file for peaks%s\n", filename);
+        return false;
+    }
+
+    for (i = 0; i < peaks->n; i++) {
+        pmPeak *peak = peaks->data[i];
+        if (peak == NULL)
+            continue;
+        fprintf (f, "%5d %5d  %7.1f %7.2f %7.2f %7.2f\n",
+                 peak->x, peak->y, peak->value, peak->SN, peak->xf, peak->yf);
+    }
+    fclose (f);
+    return true;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_SMPDATA.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_SMPDATA.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_SMPDATA.c	(revision 15358)
@@ -0,0 +1,195 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.11 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourceIO.h"
+
+// elixir-style FITS table output (header + table in 1st extension)
+// this format consists of a header derived from the image header
+// followed by a zero-size matrix, followed by the table data
+bool pmSourcesWrite_SMPDATA (psFits *fits, psArray *sources, psMetadata *imageHeader, psMetadata *tableHeader, char *extname)
+{
+
+    psArray *table;
+    psMetadata *row;
+    int i;
+    psF32 *PAR, *dPAR;
+    bool status;
+    psEllipseAxes axes;
+    psF32 xPos, yPos;
+
+    // find config information for output header
+    float ZERO_POINT = psMetadataLookupF32 (&status, imageHeader, "ZERO_PT");
+    if (!status)
+        ZERO_POINT = 25.0;
+
+    float lsky = 0;
+    int type = 0;
+
+    table = psArrayAllocEmpty (sources->n);
+
+    for (i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+
+	// no difference between PSF and non-PSF model
+        pmModel *model = pmSourceGetModel (NULL, source);
+        if (model != NULL) {
+	    PAR = model->params->data.F32;
+	    dPAR = model->dparams->data.F32;
+	    xPos = PAR[PM_PAR_XPOS];
+	    yPos = PAR[PM_PAR_YPOS];
+
+	    type = pmSourceGetDophotType (source);
+	    lsky = (source->sky < 1.0) ? 0.0 : log10(source->sky);
+
+	    axes = pmPSF_ModelToAxes (PAR, 20.0);
+
+	} else {
+	    xPos = source->peak->xf;
+	    yPos = source->peak->yf;
+	    axes.major = 0.0;
+	    axes.minor = 0.0;
+	    axes.theta = 0.0;
+	}
+
+        row = psMetadataAlloc ();
+        psMetadataAdd (row, PS_LIST_TAIL, "X_PIX",   PS_DATA_F32, "", xPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_PIX",   PS_DATA_F32, "", yPos);
+        psMetadataAdd (row, PS_LIST_TAIL, "MAG_RAW", PS_DATA_F32, "", PS_MIN (99.0, source->psfMag + ZERO_POINT));
+        psMetadataAdd (row, PS_LIST_TAIL, "MAG_ERR", PS_DATA_F32, "", PS_MIN (999, 1000*source->errMag));
+        psMetadataAdd (row, PS_LIST_TAIL, "MAG_GAL", PS_DATA_F32, "", PS_MIN (99.0, source->extMag + ZERO_POINT));
+        psMetadataAdd (row, PS_LIST_TAIL, "MAG_AP",  PS_DATA_F32, "", PS_MIN (99.0, source->apMag + ZERO_POINT));
+        psMetadataAdd (row, PS_LIST_TAIL, "LOG_SKY", PS_DATA_F32, "", lsky);
+        psMetadataAdd (row, PS_LIST_TAIL, "FWHM_X",  PS_DATA_F32, "", axes.major);
+        psMetadataAdd (row, PS_LIST_TAIL, "FWHM_Y",  PS_DATA_F32, "", axes.minor);
+        psMetadataAdd (row, PS_LIST_TAIL, "THETA",   PS_DATA_F32, "", axes.theta);
+        psMetadataAdd (row, PS_LIST_TAIL, "DOPHOT",  PS_DATA_U8,  "", type);
+        psMetadataAdd (row, PS_LIST_TAIL, "WEIGHT",  PS_DATA_U8,  "", PS_MIN (255, PS_MAX(0, 255*source->pixWeight)));
+        psMetadataAdd (row, PS_LIST_TAIL, "DUMMY",   PS_DATA_U16, "", 0);
+
+        psArrayAdd (table, 100, row);
+        psFree (row);
+    }
+
+    if (table->n == 0) {
+        psFitsWriteBlank (fits, tableHeader, extname);
+        psFree (table);
+        return true;
+    }
+
+    psTrace ("pmFPAfile", 5, "writing ext data %s\n", extname);
+    if (!psFitsWriteTable (fits, tableHeader, table, extname)) {
+        psError(PS_ERR_IO, false, "writing ext data %s\n", extname);
+        psFree(table);
+        return false;
+    }
+
+    psFree (table);
+    return true;
+}
+
+// read in a readout from the fits file
+psArray *pmSourcesRead_SMPDATA (psFits *fits, psMetadata *header)
+{
+
+    bool status;
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+    float lsky;
+
+    // define PSF model type
+    int modelType = pmModelClassGetType ("PS_MODEL_GAUSS");
+
+    char *PSF_NAME = psMetadataLookupStr (&status, header, "PSF_NAME");
+    if (PSF_NAME != NULL) {
+        modelType = pmModelClassGetType (PSF_NAME);
+    }
+
+    // find config information for output header
+    float ZERO_POINT = psMetadataLookupF32 (&status, header, "ZERO_PT");
+    if (!status)
+        ZERO_POINT = 25.0;
+
+    psArray *table = psFitsReadTable (fits);
+    // validate a single row of the table (must match SMP)
+
+    psArray *sources = psArrayAlloc (table->n);
+
+    // convert the table to the pmSource entries
+    // XXX need to chooose PSF vs EXT, based on type?
+    for (int i = 0; i < table->n; i++) {
+        pmSource *source = pmSourceAlloc ();
+        pmModel *model = pmModelAlloc (modelType);
+        source->modelPSF  = model;
+        source->type = PM_SOURCE_TYPE_STAR;
+
+        PAR = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        psMetadata *row = table->data[i];
+
+        lsky             = psMetadataLookupF32 (&status, row, "LOG_SKY");
+        PAR[PM_PAR_SKY]  = pow(10.0, lsky);
+        source->sky    = PAR[PM_PAR_SKY];
+
+        PAR[PM_PAR_XPOS] = psMetadataLookupF32 (&status, row, "X_PIX");
+        PAR[PM_PAR_YPOS] = psMetadataLookupF32 (&status, row, "Y_PIX");
+        axes.major       = psMetadataLookupF32 (&status, row, "FWHM_X");
+        axes.minor       = psMetadataLookupF32 (&status, row, "FWHM_Y");
+        axes.theta       = psMetadataLookupF32 (&status, row, "THETA");
+
+	pmPSF_AxesToModel (PAR, axes);
+
+
+        source->psfMag = psMetadataLookupF32 (&status, row, "MAG_RAW") - ZERO_POINT;
+        source->extMag = psMetadataLookupF32 (&status, row, "MAG_GAL") - ZERO_POINT;
+        source->errMag = psMetadataLookupF32 (&status, row, "MAG_ERR") * 0.001;
+        source->apMag  = psMetadataLookupF32 (&status, row, "MAG_AP")  - ZERO_POINT;
+
+        source->pixWeight = psMetadataLookupU8 (&status, row, "WEIGHT")/255.0;
+        int dophot = psMetadataLookupU8 (&status, row, "DOPHOT");
+	pmSourceSetDophotType (source, dophot);
+
+	double Area = 2.0 * M_PI * axes.major * axes.minor;
+	double peakFlux = source->psfMag / Area;
+
+	source->peak = pmPeakAlloc(PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], peakFlux, PM_PEAK_LONE);
+        sources->data[i] = source;
+    }
+    psFree (table);
+    return (sources);
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_SX.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_SX.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceIO_SX.c	(revision 15358)
@@ -0,0 +1,93 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourceIO.h"
+
+// elixir-mode / sextractor-style output list with fixed line width
+bool pmSourcesWriteSX (psArray *sources, char *filename)
+{
+
+    psF32 *PAR, *dPAR;
+    psEllipseAxes axes;
+
+    psLine *line = psLineAlloc (110);  // 110 is sextractor line length
+
+    FILE *f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg (__func__, 3, "can't open output file for output %s\n", filename);
+        return false;
+    }
+
+    // write sources with models
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+
+	// no difference between PSF and non-PSF model
+        pmModel *model = pmSourceGetModel (NULL, source);
+        if (model == NULL)
+            continue;
+
+        PAR = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        // pmSourceSextractType (source, &type, &flags);
+
+	axes = pmPSF_ModelToAxes (PAR, 20.0);
+
+        psLineInit (line);
+        psLineAdd (line, "%5.2f",  0.0); // should be type
+        psLineAdd (line, "%11.3f", PAR[PM_PAR_XPOS]);
+        psLineAdd (line, "%11.3f", PAR[PM_PAR_YPOS]);
+        psLineAdd (line, "%9.4f",  source->psfMag);
+        psLineAdd (line, "%9.4f",  source->errMag);
+        psLineAdd (line, "%13.4f", source->sky);
+        psLineAdd (line, "%9.2f",  axes.major);
+        psLineAdd (line, "%9.2f",  axes.minor);
+        psLineAdd (line, "%6.1f",  axes.theta);
+        psLineAdd (line, "%9.4f",  source->extMag);
+        psLineAdd (line, "%9.4f",  source->apMag);
+        psLineAdd (line, "%4d\n",  0); // should be flags
+        fwrite (line->line, 1, line->Nline, f);
+    }
+    fclose (f);
+    psFree (line);
+    return true;
+}
+
+// XXX need to fix the FWHM / shape stuff,
+// XXX make sure we are using the correct mags, etc
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourcePhotometry.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourcePhotometry.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourcePhotometry.c	(revision 15358)
@@ -0,0 +1,784 @@
+/** @file  pmSourcePhotometry.c
+ *
+ *  @author EAM, IfA; GLG, MHPCC
+ *
+ *  @version $Revision: 1.36 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-29 03:14:55 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmErrorCodes.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAMaskWeight.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmModelClass.h"
+#include "pmSourcePhotometry.h"
+
+# define DO_SKY 0
+
+static float AP_MIN_SN = 0.0;
+
+bool pmSourceMagnitudesInit (psMetadata *config)
+{
+
+    bool status;
+
+    float limit = psMetadataLookupF32 (&status, config, "AP_MIN_SN");
+    if (status) {
+        AP_MIN_SN = limit;
+    }
+    return true;
+}
+
+/**
+    this function is used to calculate the three defined source magnitudes:
+    - apMag  : only if S/N > AP_MIN_SN
+             : is optionally corrected for curve-of-growth if:
+        - the source is a STAR (PSF)
+        - the option is selected (mode & PM_SOURCE_PHOT_GROWTH)
+    - psfMag : all sources with non-NULL modelPSF
+             : is optionally corrected for aperture residual if:
+        - the source is a STAR (PSF)
+        - the option is selected (mode & PM_SOURCE_PHOT_APCORR)
+
+    - extMag : all sources with non-NULL modelEXT
+**/
+
+// XXX masked region should be (optionally) elliptical
+bool pmSourceMagnitudes (pmSource *source, pmPSF *psf, pmSourcePhotometryMode mode, psMaskType maskVal, psMaskType mark)
+{
+
+    int status = false;
+    bool isPSF;
+    float x, y;
+    float rflux;
+    float SN;
+    pmModel *model;
+
+    source->psfMag = NAN;
+    source->extMag = NAN;
+    source->errMag = NAN;
+    source->apMag  = NAN;
+
+    // we must have a valid model
+    model = pmSourceGetModel (&isPSF, source);
+    if (model == NULL) {
+	psTrace ("psModules.objects", 3, "fail mag : no valid model");
+        return false;
+    }
+
+    if (model->dparams->data.F32[PM_PAR_I0] > 0) {
+        SN = model->params->data.F32[PM_PAR_I0] / model->dparams->data.F32[PM_PAR_I0];
+        source->errMag = 1.0 / SN;
+    } else {
+        SN = NAN;
+        source->errMag = NAN;
+    }
+    x = model->params->data.F32[PM_PAR_XPOS];
+    y = model->params->data.F32[PM_PAR_YPOS];
+
+    // measure PSF model photometry
+    if (psf->FluxScale) {
+	// the source peak pixel is guaranteed to be on the image, and only minimally different from the source center
+	double fluxScale = pmTrend2DEval (psf->FluxScale, (float)source->peak->x, (float)source->peak->y);
+	if (!isfinite(fluxScale)) {
+	    // XXX objects on the edge can be slightly outside -- if we get an 
+	    // error, use the full fit.
+	    psErrorClear();
+	    status = pmSourcePhotometryModel (&source->psfMag, source->modelPSF);
+	} else {
+	    if (isfinite(fluxScale) && (fluxScale > 0.0)) {
+		source->psfMag = -2.5*log10(fluxScale * source->modelPSF->params->data.F32[PM_PAR_I0]);
+	    } else {
+		source->psfMag = NAN;
+	    }
+	}
+    } else {
+	status = pmSourcePhotometryModel (&source->psfMag, source->modelPSF);
+    }
+
+    // measure EXT model photometry
+    status = pmSourcePhotometryModel (&source->extMag, source->modelEXT);
+
+    // for PSFs, correct both apMag and psfMag to same system, consistent with infinite flux star in aperture RADIUS
+    if ((mode & PM_SOURCE_PHOT_APCORR) && isPSF && psf && psf->ApTrend) {
+	// the source peak pixel is guaranteed to be on the image, and only minimally different from the source center
+	double apTrend = pmTrend2DEval (psf->ApTrend, (float)source->peak->x, (float)source->peak->y);
+        source->psfMag += apTrend;
+    }
+
+    if (!isfinite(SN) || (SN < AP_MIN_SN)) {
+	psTrace ("psModules.objects", 3, "fail mag : bad SN: %f (limit: %f)", SN, AP_MIN_SN);
+        return false;
+    }
+
+    // replace source flux
+    // XXX need to be certain which model and size of mask for prior subtraction
+    // XXX full model or just analytical?
+    // XXX use pmSourceAdd instead?
+    if (source->mode & PM_SOURCE_MODE_SUBTRACTED) {
+        pmModelAdd (source->pixels, source->maskObj, model, PM_MODEL_OP_FULL, maskVal);
+    }
+
+    // if we are measuring aperture photometry and applying the growth correction,
+    // we need to shift the flux in the selected pixels (but not the mask)
+    // psImageShift ();
+    psImage *flux;
+    if (mode & PM_SOURCE_PHOT_INTERP) {
+        float dx = 0.5 - x + (int)x;
+        float dy = 0.5 - y + (int)y;
+        x += dx;
+        y += dy;
+        flux = psImageShift (NULL, source->pixels, dx, dy, 0.0, PS_INTERPOLATE_BICUBE);
+        // flux = psImageShift (NULL, source->pixels, dx, dy, 0.0, PS_INTERPOLATE_BILINEAR);
+
+        // XXX this is test code to verify the shift is doing the right thing (seems to be)
+        # if (0)
+            // measure centroid of unshifted gaussian (should be 16.0,16.0)
+        {
+            psImage *image = source->pixels;
+            float xo = 0.0;
+            float yo = 0.0;
+            float xo2 = 0.0;
+            float yo2 = 0.0;
+            float no = 0.0;
+            for (int j = 0; j < image->numRows; j++)
+            {
+                for (int i = 0; i < image->numCols; i++) {
+                    xo += i*image->data.F32[j][i];
+                    yo += j*image->data.F32[j][i];
+                    xo2 += i*i*image->data.F32[j][i];
+                    yo2 += j*j*image->data.F32[j][i];
+                    no += image->data.F32[j][i];
+                }
+            }
+            xo /= no;
+            yo /= no;
+            xo2 = sqrt (xo2/no - xo*xo);
+            yo2 = sqrt (yo2/no - yo*yo);
+            fprintf (stderr, "pre-shift centroid: %f,%f, sigma: %f,%f: flux: %f\n", xo, yo, xo2, yo2, no);
+        }
+
+        // measure centroid of unshifted gaussian (should be 16.0,16.0)
+        {
+            psImage *image = flux;
+            float xo = 0.0;
+            float yo = 0.0;
+            float xo2 = 0.0;
+            float yo2 = 0.0;
+            float no = 0.0;
+            for (int j = 0; j < image->numRows; j++)
+            {
+                for (int i = 0; i < image->numCols; i++) {
+                    xo += i*image->data.F32[j][i];
+                    yo += j*image->data.F32[j][i];
+                    xo2 += i*i*image->data.F32[j][i];
+                    yo2 += j*j*image->data.F32[j][i];
+                    no += image->data.F32[j][i];
+                }
+            }
+            xo /= no;
+            yo /= no;
+            xo2 = sqrt (xo2/no - xo*xo);
+            yo2 = sqrt (yo2/no - yo*yo);
+            fprintf (stderr, "pre-shift centroid: %f,%f, sigma: %f,%f: flux: %f\n", xo, yo, xo2, yo2, no);
+        }
+        # endif
+
+    } else {
+        flux = source->pixels;
+    }
+    // set aperture mask circle to model radius
+    // XXX use a different radius for apertures and fits...
+    // XXX can I remove this?  the source should have the mask defined when it is constructed or
+    // when the fit / aperture radius is changed...
+    psImageKeepCircle (source->maskObj, x, y, model->radiusFit, "OR", mark);
+
+    // measure the weight of included pixels
+    // XXX is this supposed to use the weight or the flux?
+    if (mode & PM_SOURCE_PHOT_WEIGHT) {
+        pmSourcePixelWeight (&source->pixWeight, model, source->pixels, source->maskObj, maskVal);
+    }
+
+    // measure object aperture photometry
+    status = pmSourcePhotometryAper  (&source->apMag, model, flux, source->maskObj, maskVal);
+    if (!status) {
+	psTrace ("psModules.objects", 3, "fail mag : bad Ap Mag");
+        psErrorCode last = psErrorCodeLast();
+	// XXX flag the object?
+        if (last == PM_ERR_PHOTOM) {
+            // the aper mag was undefined (flux < 0).  bad object, but
+            // don't keep the error on the stack
+            psErrorClear();
+        }
+    }
+
+    // for PSFs, correct both apMag and psfMag to same system, consistent with infinite flux star in aperture RADIUS
+    if (status && isPSF && psf) {
+        if (psf->growth && (mode & PM_SOURCE_PHOT_GROWTH)) {
+            source->apMag += pmGrowthCurveCorrect (psf->growth, model->radiusFit);
+        }
+        if (mode & PM_SOURCE_PHOT_APCORR) {
+            rflux   = pow (10.0, 0.4*source->psfMag);
+            source->apMag  -= PS_SQR(model->radiusFit)*rflux * psf->skyBias + psf->skySat / rflux;
+        }
+    }
+    if (mode & PM_SOURCE_PHOT_INTERP) {
+        psFree (flux);
+    }
+
+    // unmask aperture
+    // XXX can I remove this?  this will probably break things downstream...
+    psImageKeepCircle (source->maskObj, x, y, model->radiusFit, "AND", PS_NOT_U8(mark));
+
+    // if source was originally subtracted, re-subtract object, leave local sky
+    // XXX replace with pmSourceSub...
+    if (source->mode & PM_SOURCE_MODE_SUBTRACTED) {
+        pmModelSub (source->pixels, source->maskObj, model, PM_MODEL_OP_FULL, maskVal);
+    }
+
+    return status;
+}
+
+/*
+aprMag' - fitMag = flux*skySat + r^2*rflux*skyBias + ApTrend(x,y)
+(aprMag - flux*skySat - r^2*rflux*skyBias) - fitMAg = ApTrend(x,y)
+(aprMag - flux*skySat - r^2*rflux*skyBias) = fitMAg + ApTrend(x,y)
+
+*/
+
+// return source model magnitude
+bool pmSourcePhotometryModel (float *fitMag, pmModel *model)
+{
+
+    float fitSum = 0;
+    *fitMag = NAN;
+
+    if (model == NULL) {
+        return false;
+    }
+
+    // measure fitMag
+    fitSum = model->modelFlux (model->params);
+    if (fitSum <= 0)
+        return false;
+    if (!isfinite(fitSum))
+        return false;
+    *fitMag = -2.5*log10(fitSum);
+
+    return (true);
+}
+
+// return source aperture magnitude
+bool pmSourcePhotometryAper (float *apMag, pmModel *model, psImage *image, psImage *mask, psMaskType maskVal)
+{
+    float apSum = 0;
+    float sky = 0;
+    *apMag = NAN;
+
+    if (model == NULL) {
+        psError(PM_ERR_OBJECTS, true, "Model is NULL");
+        return false;
+    }
+
+    if (DO_SKY) {
+        sky = model->params->data.F32[PM_PAR_SKY];
+    } else {
+        sky = 0;
+    }
+
+    psF32 **imData = image->data.F32;
+    psU8 **mkData = mask->data.U8;
+
+    // measure apMag
+    for (int ix = 0; ix < image->numCols; ix++) {
+        for (int iy = 0; iy < image->numRows; iy++) {
+            if (mkData[iy][ix] & maskVal)
+                continue;
+            apSum += imData[iy][ix] - sky;
+        }
+    }
+    if (apSum <= 0) {
+        psError(PM_ERR_PHOTOM, true, "apSum is negative");
+        return false;
+    }
+
+    *apMag = -2.5*log10(apSum);
+    return (true);
+}
+
+// return source aperture magnitude
+bool pmSourcePixelWeight (float *pixWeight, pmModel *model, psImage *image, psImage *mask, psMaskType maskVal)
+{
+    float modelSum = 0;
+    float validSum = 0;
+    float sky = 0;
+    float value;
+
+    int Xo, Yo, dP;
+    int dX, DX, NX;
+    int dY, DY, NY;
+
+    *pixWeight = 0.0;
+
+    if (model == NULL)
+        return false;
+
+    // we only care about the value of the object model, not the local sky
+    if (DO_SKY) {
+        sky = model->params->data.F32[PM_PAR_SKY];
+    } else {
+        sky = 0;
+    }
+
+    // the model function returns the source flux at a position
+    psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+
+    psVector *params = model->params;
+
+    Xo = params->data.F32[PM_PAR_XPOS];
+    Yo = params->data.F32[PM_PAR_YPOS];
+
+    dX = Xo - image->col0;
+    dP = image->numCols - dX;
+    DX = PS_MAX(dX, dP);
+    NX = image->numCols;
+
+    dY = Yo - image->row0;
+    dP = image->numRows - dY;
+    DY = PS_MAX(dY, dP);
+    NY = image->numRows;
+
+    // measure modelSum and validSum
+    // XXX this does not work for sources near the edge: we need to measure for
+    // a full square region
+    for (int ix = -DX; ix < DX + 1; ix++) {
+        int mx = ix + dX;
+        for (int iy = -DY; iy < DY + 1; iy++) {
+            int my = iy + dY;
+
+            coord->data.F32[0] = (psF32) (ix + Xo);
+            coord->data.F32[1] = (psF32) (iy + Yo);
+
+            // for the full model, add all points
+            value = model->modelFunc (NULL, params, coord) - sky;
+            modelSum += value;
+
+            // include count only the unmasked pixels within the image area
+            if (mx < 0)
+                continue;
+            if (my < 0)
+                continue;
+            if (mx >= NX)
+                continue;
+            if (my >= NY)
+                continue;
+            if (mask->data.U8[my][mx] & maskVal)
+                continue;
+
+            validSum += value;
+        }
+    }
+    psFree (coord);
+
+    if (validSum <= 0)
+        return false;
+
+    *pixWeight = validSum / modelSum;
+    return (true);
+}
+
+# if (0)
+double pmSourceCrossProduct (const pmSource *Mi,
+                             const pmSource *Mj,
+                             const bool unweighted_sum) // should the cross product be weighted?
+{
+
+    int Xs, Xe, Ys, Ye;
+    int xi, xj, yi, yj;
+    int xIs, xJs, yIs, yJs;
+    int xIe, yIe;
+    double flux, wt;
+
+    const psImage *Pi = Mi->pixels;
+    assert (Pi != NULL);
+    const psImage *Pj = Mj->pixels;
+    assert (Pj != NULL);
+
+    const psImage *Wi = Mi->weight;
+    if (!unweighted_sum) {
+        assert (Wi != NULL);
+    }
+
+    const psImage *Ti = Mi->maskObj;
+    assert (Ti != NULL);
+    const psImage *Tj = Mj->maskObj;
+    assert (Tj != NULL);
+
+    Xs = PS_MAX (Pi->col0, Pj->col0);
+    Xe = PS_MIN (Pi->col0 + Pi->numCols, Pj->col0 + Pj->numCols);
+
+    Ys = PS_MAX (Pi->row0, Pj->row0);
+    Ye = PS_MIN (Pi->row0 + Pi->numRows, Pj->row0 + Pj->numRows);
+
+    xIs = Xs - Pi->col0;
+    xJs = Xs - Pj->col0;
+    yIs = Ys - Pi->row0;
+    yJs = Ys - Pj->row0;
+
+    xIe = Xe - Pi->col0;
+    yIe = Ye - Pi->row0;
+
+    // note that this is addressing the same image pixels,
+    // though only if both are source not model images
+    flux = 0;
+    for (yi = yIs, yj = yJs; yi < yIe; yi++, yj++) {
+        for (xi = xIs, xj = xJs; xi < xIe; xi++, xj++) {
+            if (Ti->data.U8[yi][xi])
+                continue;
+            if (Tj->data.U8[yj][xj])
+                continue;
+
+            if (unweighted_sum) {
+                flux += (Pi->data.F32[yi][xi] * Pj->data.F32[yj][xj]);
+            } else {
+                wt = Wi->data.F32[yi][xi];
+                if (wt > 0) {
+                    flux += (Pi->data.F32[yi][xi] * Pj->data.F32[yj][xj]) / wt;
+                }
+            }
+        }
+    }
+    return flux;
+}
+
+double pmSourceCrossWeight(const pmSource *Mi,
+                           const pmSource *Mj,
+                           const bool unweighted_sum) // should the cross product be weighted?
+{
+
+    int Xs, Xe, Ys, Ye;
+    int xi, xj, yi, yj;
+    int xIs, xJs, yIs, yJs;
+    int xIe, yIe;
+    double flux, wt;
+
+    const psImage *Pi = Mi->pixels;
+    assert (Pi != NULL);
+    const psImage *Pj = Mj->pixels;
+    assert (Pj != NULL);
+
+    const psImage *Wi = Mi->weight;
+    if (!unweighted_sum) {
+        assert (Wi != NULL);
+    }
+
+    const psImage *Ti = Mi->maskObj;
+    assert (Ti != NULL);
+    const psImage *Tj = Mj->maskObj;
+    assert (Tj != NULL);
+
+    Xs = PS_MAX (Pi->col0, Pj->col0);
+    Xe = PS_MIN (Pi->col0 + Pi->numCols, Pj->col0 + Pj->numCols);
+
+    Ys = PS_MAX (Pi->row0, Pj->row0);
+    Ye = PS_MIN (Pi->row0 + Pi->numRows, Pj->row0 + Pj->numRows);
+
+    xIs = Xs - Pi->col0;
+    xJs = Xs - Pj->col0;
+    yIs = Ys - Pi->row0;
+    yJs = Ys - Pj->row0;
+
+    xIe = Xe - Pi->col0;
+    yIe = Ye - Pi->row0;
+
+    // note that this is addressing the same image pixels,
+    // though only if both are source not model images
+    flux = 0;
+    for (yi = yIs, yj = yJs; yi < yIe; yi++, yj++) {
+        for (xi = xIs, xj = xJs; xi < xIe; xi++, xj++) {
+            if (Ti->data.U8[yi][xi])
+                continue;
+            if (Tj->data.U8[yj][xj])
+                continue;
+
+            if (unweighted_sum) {
+                flux++;
+            } else {
+                wt = Wi->data.F32[yi][xi];
+                if (wt > 0) {
+                    flux += 1.0 / wt;
+                }
+            }
+        }
+    }
+    return flux;
+}
+
+double pmSourceWeight(const pmSource *Mi,
+                      int term,
+                      const bool unweighted_sum) // should the cross product be weighted?
+{
+    double flux = 0, wt = 0, factor = 0;
+
+    const psImage *Pi = Mi->pixels;
+    assert (Pi != NULL);
+    const psImage *Wi = Mi->weight;
+    if (!unweighted_sum) {
+        assert (Wi != NULL);
+    }
+    const psImage *Ti = Mi->maskObj;
+    assert (Ti != NULL);
+
+    // note that this is addressing the same image pixels,
+    // though only if both are source not model images
+    for (int yi = 0; yi < Pi->numRows; yi++) {
+        for (int xi = 0; xi < Pi->numCols; xi++) {
+            if (Ti->data.U8[yi][xi])
+                continue;
+            if (!unweighted_sum) {
+                wt = Wi->data.F32[yi][xi];
+                if (wt == 0)
+                    continue;
+            }
+
+            switch (term) {
+            case 0:
+                factor = 1;
+                break;
+            case 1:
+                factor = xi + Pi->col0;
+                break;
+            case 2:
+                factor = yi + Pi->row0;
+                break;
+            default:
+                psAbort("invalid term for pmSourceWeight");
+            }
+
+            if (unweighted_sum) {
+                flux += (factor * Pi->data.F32[yi][xi]);
+            } else {
+                flux += (factor * Pi->data.F32[yi][xi]) / wt;
+            }
+            // fprintf (stderr, "Pi: %f, flux: %f\n", Pi->data.F32[yi][xi], flux);
+        }
+    }
+    return flux;
+}
+# endif
+
+bool pmSourceChisq (pmModel *model, psImage *image, psImage *mask, psImage *weight, psMaskType maskVal)
+{
+
+    double dC = 0.0;
+    int Npix = 0;
+    for (int j = 0; j < image->numRows; j++) {
+        for (int i = 0; i < image->numCols; i++) {
+            if (mask->data.U8[j][i] & maskVal)
+                continue;
+            if (weight->data.F32[j][i] <= 0)
+                continue;
+            dC += PS_SQR (image->data.F32[j][i]) / weight->data.F32[j][i];
+            Npix ++;
+        }
+    }
+    model->nDOF = Npix - 1;
+    model->chisq = dC;
+
+    return (true);
+}
+
+
+double pmSourceModelWeight(const pmSource *Mi,
+                      int term,
+                      const bool unweighted_sum) // should the cross product be weighted?
+{
+    double flux = 0, wt = 0, factor = 0;
+
+    const psImage *Pi = Mi->modelFlux;
+    assert (Pi != NULL);
+    const psImage *Wi = Mi->weight;
+    if (!unweighted_sum) {
+        assert (Wi != NULL);
+    }
+    const psImage *Ti = Mi->maskObj;
+    assert (Ti != NULL);
+
+    for (int yi = 0; yi < Pi->numRows; yi++) {
+        for (int xi = 0; xi < Pi->numCols; xi++) {
+            if (Ti->data.U8[yi][xi])
+                continue;
+            if (!unweighted_sum) {
+                wt = Wi->data.F32[yi][xi];
+                if (wt == 0)
+                    continue;
+            }
+
+            switch (term) {
+            case 0:
+                factor = 1;
+                break;
+            case 1:
+                factor = xi + Pi->col0;
+                break;
+            case 2:
+                factor = yi + Pi->row0;
+                break;
+            default:
+                psAbort("invalid term for pmSourceWeight");
+            }
+
+            if (unweighted_sum) {
+                flux += (factor * Pi->data.F32[yi][xi]);
+            } else {
+                flux += (factor * Pi->data.F32[yi][xi]) / wt;
+            }
+        }
+    }
+    return flux;
+}
+
+double pmSourceModelDotModel (const pmSource *Mi,
+                              const pmSource *Mj,
+                              const bool unweighted_sum) // should the cross product be weighted?
+{
+    int Xs, Xe, Ys, Ye;
+    int xi, xj, yi, yj;
+    int xIs, xJs, yIs, yJs;
+    int xIe, yIe;
+    double flux, wt;
+
+    const psImage *Pi = Mi->modelFlux;
+    assert (Pi != NULL);
+    const psImage *Pj = Mj->modelFlux;
+    assert (Pj != NULL);
+
+    const psImage *Wi = Mi->weight;
+    if (!unweighted_sum) {
+        assert (Wi != NULL);
+    }
+
+    const psImage *Ti = Mi->maskObj;
+    assert (Ti != NULL);
+    const psImage *Tj = Mj->maskObj;
+    assert (Tj != NULL);
+
+    Xs = PS_MAX (Pi->col0, Pj->col0);
+    Xe = PS_MIN (Pi->col0 + Pi->numCols, Pj->col0 + Pj->numCols);
+
+    Ys = PS_MAX (Pi->row0, Pj->row0);
+    Ye = PS_MIN (Pi->row0 + Pi->numRows, Pj->row0 + Pj->numRows);
+
+    xIs = Xs - Pi->col0;
+    xJs = Xs - Pj->col0;
+    yIs = Ys - Pi->row0;
+    yJs = Ys - Pj->row0;
+
+    xIe = Xe - Pi->col0;
+    yIe = Ye - Pi->row0;
+
+    // note that weight is addressing the same image pixels
+    flux = 0;
+    for (yi = yIs, yj = yJs; yi < yIe; yi++, yj++) {
+        for (xi = xIs, xj = xJs; xi < xIe; xi++, xj++) {
+            if (Ti->data.U8[yi][xi])
+                continue;
+            if (Tj->data.U8[yj][xj])
+                continue;
+
+            // XXX skip the nonsense weight pixels?
+            if (unweighted_sum) {
+                flux += (Pi->data.F32[yi][xi] * Pj->data.F32[yj][xj]);
+            } else {
+                wt = Wi->data.F32[yi][xi];
+                if (wt > 0) {
+                    flux += (Pi->data.F32[yi][xi] * Pj->data.F32[yj][xj]) / wt;
+                }
+            }
+        }
+    }
+    return flux;
+}
+
+double pmSourceDataDotModel (const pmSource *Mi,
+                             const pmSource *Mj,
+                             const bool unweighted_sum) // should the cross product be weighted?
+{
+
+    int Xs, Xe, Ys, Ye;
+    int xi, xj, yi, yj;
+    int xIs, xJs, yIs, yJs;
+    int xIe, yIe;
+    double flux, wt;
+
+    const psImage *Pi = Mi->pixels;
+    assert (Pi != NULL);
+    const psImage *Pj = Mj->modelFlux;
+    assert (Pj != NULL);
+
+    const psImage *Wi = Mi->weight;
+    if (!unweighted_sum) {
+        assert (Wi != NULL);
+    }
+
+    const psImage *Ti = Mi->maskObj;
+    assert (Ti != NULL);
+    const psImage *Tj = Mj->maskObj;
+    assert (Tj != NULL);
+
+    Xs = PS_MAX (Pi->col0, Pj->col0);
+    Xe = PS_MIN (Pi->col0 + Pi->numCols, Pj->col0 + Pj->numCols);
+
+    Ys = PS_MAX (Pi->row0, Pj->row0);
+    Ye = PS_MIN (Pi->row0 + Pi->numRows, Pj->row0 + Pj->numRows);
+
+    xIs = Xs - Pi->col0;
+    xJs = Xs - Pj->col0;
+    yIs = Ys - Pi->row0;
+    yJs = Ys - Pj->row0;
+
+    xIe = Xe - Pi->col0;
+    yIe = Ye - Pi->row0;
+
+    // note that weight is addressing the same image pixels,
+    flux = 0;
+    for (yi = yIs, yj = yJs; yi < yIe; yi++, yj++) {
+        for (xi = xIs, xj = xJs; xi < xIe; xi++, xj++) {
+            if (Ti->data.U8[yi][xi])
+                continue;
+            if (Tj->data.U8[yj][xj])
+                continue;
+
+            // XXX skip the nonsense weight pixels?
+            if (unweighted_sum) {
+                flux += (Pi->data.F32[yi][xi] * Pj->data.F32[yj][xj]);
+            } else {
+                wt = Wi->data.F32[yi][xi];
+                if (wt > 0) {
+                    flux += (Pi->data.F32[yi][xi] * Pj->data.F32[yj][xj]) / wt;
+                }
+            }
+        }
+    }
+    return flux;
+}
+
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourcePhotometry.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourcePhotometry.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourcePhotometry.h	(revision 15358)
@@ -0,0 +1,67 @@
+/* @file  pmSourcePhotometry.h
+ * @brief functions to measure source photometry
+ *
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.10 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-06-20 02:22:26 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_SOURCE_PHOTOMETRY_H
+# define PM_SOURCE_PHOTOMETRY_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/**
+ *
+ * The function returns both the magnitude of the fit, defined as -2.5log(flux),
+ * where the flux is integrated under the model, theoretically from a radius of 0
+ * to infinity. In practice, we integrate the model beyond 50sigma.  The aperture magnitude is
+ * defined as -2.5log(flux) , where the flux is summed for all pixels which are
+ * not excluded by the aperture mask. The model flux is calculated by calling the
+ * model-specific function provided by pmModelFlux_GetFunction.
+ *
+ * XXX: must code this.
+ *
+ */
+
+typedef enum {
+    PM_SOURCE_PHOT_NONE   = 0x0000,
+    PM_SOURCE_PHOT_GROWTH = 0x0001,
+    PM_SOURCE_PHOT_APCORR = 0x0002,
+    PM_SOURCE_PHOT_WEIGHT = 0x0004,
+    PM_SOURCE_PHOT_INTERP = 0x0008,
+} pmSourcePhotometryMode;
+
+bool pmSourcePhotometryModel(
+    float *fitMag,                      ///< integrated fit magnitude
+    pmModel *model                      ///< model used for photometry
+);
+
+bool pmSourcePhotometryAper(
+    float   *apMag,                     ///< aperture flux magnitude
+    pmModel *model,                     ///< model used for photometry
+    psImage *image,                     ///< image pixels to be used
+    psImage *mask,                      ///< mask of pixels to ignore
+    psMaskType maskVal                  ///< Value to mask
+);
+
+bool pmSourceMagnitudesInit (psMetadata *config);
+bool pmSourceMagnitudes (pmSource *source, pmPSF *psf, pmSourcePhotometryMode mode, psMaskType maskVal, psMaskType mark);
+bool pmSourcePixelWeight (float *pixWeight, pmModel *model, psImage *image, psImage *mask, psMaskType maskVal);
+bool pmSourceChisq (pmModel *model, psImage *image, psImage *mask, psImage *weight, psMaskType maskVal);
+
+
+double pmSourceDataDotModel (const pmSource *Mi, const pmSource *Mj, const bool unweighted_sum);
+double pmSourceModelDotModel (const pmSource *Mi, const pmSource *Mj, const bool unweighted_sum);
+double pmSourceModelWeight(const pmSource *Mi, int term, const bool unweighted_sum);
+
+// retire these:
+// double pmSourceCrossProduct(const pmSource *Mi, const pmSource *Mj, const bool unweighted_sum);
+// double pmSourceCrossWeight(const pmSource *Mi, const pmSource *Mj, const bool unweighted_sum);
+// double pmSourceWeight(const pmSource *Mi, int term, const bool unweighted_sum);
+
+/// @}
+# endif /* PM_SOURCE_PHOTOMETRY_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlotApResid.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlotApResid.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlotApResid.c	(revision 15358)
@@ -0,0 +1,158 @@
+/** @file  pmSourcePlot.c
+ *
+ *  Plot the Aperture Mag - Fitted Mag residuals
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *  Copyright 2006 IfA, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmSourcePlots.h"
+#include "pmKapaPlots.h"
+
+// this variable is defined in psmodules.h if ohana-config is found
+# if (HAVE_KAPA)
+    # include <kapa.h>
+
+    // plot the sx, sy, sxy as vector field,
+    // plot the PSF measured sx, sy, sxy as vector field
+    // pull the sources from the config / file?
+    bool pmSourcePlotApResid (const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout)
+{
+
+    Graphdata graphdata;
+    KapaSection section;
+
+    psLogMsg ("psphot", 3, "creating ap mag - psf mag plot");
+
+    // find the currently selected readout
+    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
+
+    psArray *sources = psMetadataLookupPtr (NULL, readout->analysis, "PSPHOT.SOURCES");
+    if (sources == NULL)
+        return false;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    int DX = 1000;
+    float dx = DX / layout->nX;
+    float dy = dx * layout->aspectRatio;
+    int DY = dy * layout->nY;
+
+    // XXX make the aspect-ratio match the image
+    if (layout->i == 0) {
+        KapaResize (kapa, DX, DY);
+    }
+
+    KapaClearPlots (kapa);
+    KapaInitGraph (&graphdata);
+    section.dx = dx / DX;
+    section.dy = dy / DY;
+    section.x = layout->iX * section.dx;
+    section.y = layout->iY * section.dy;
+    section.name = NULL;
+    psStringAppend (&section.name, "a%d", layout->i);
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    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->apMag)) continue;
+	if (!isfinite (source->psfMag)) continue;
+
+        x->data.F32[n] = source->psfMag;
+        y->data.F32[n] = source->apMag - source->psfMag;
+        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]);
+
+        n++;
+    }
+    x->n = y->n = n;
+
+    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 set the plot range to match the image
+    KapaSetLimits (kapa, &graphdata);
+
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "PSF Mag", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "Ap Mag - PSF Mag", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 2;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, n, &graphdata);
+    KapaPlotVector (kapa, n, x->data.F32, "x");
+    KapaPlotVector (kapa, n, y->data.F32, "y");
+
+    if (layout->i == layout->nTotal - 1) {
+        psLogMsg ("psphot", 3, "saving plot to %s", file->filename);
+        KapaPNG (kapa, file->filename);
+	KapaClearPlots (kapa);
+    }
+
+    psFree (x);
+    psFree (y);
+    return true;
+}
+
+# else
+
+    bool pmSourcePlotApResid (const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout)
+{
+    psLogMsg ("psphot", 3, "skipping ap-mag resid plot");
+    return true;
+}
+
+# endif
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlotMoments.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlotMoments.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlotMoments.c	(revision 15358)
@@ -0,0 +1,171 @@
+/** @file  pmSourcePlot.c
+ *
+ * This file contains functions to write source plots
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2006 IfA, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmSourcePlots.h"
+#include "pmKapaPlots.h"
+
+// this variable is defined in psmodules.h if ohana-config is found
+# if (HAVE_KAPA)
+    # include <kapa.h>
+
+    // plot the sx, sy moments plane (faint and bright sources)
+    bool pmSourcePlotMoments (const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout)
+{
+
+    Graphdata graphdata;
+    KapaSection section;
+
+    psLogMsg ("psphot", 3, "creating moments plot");
+
+    // find the currently selected readout
+    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
+
+    psArray *sources = psMetadataLookupPtr (NULL, readout->analysis, "PSPHOT.SOURCES");
+    if (sources == NULL)
+        return false;
+
+    int kapa = pmKapaOpen (false);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    // moments plot subplots are square
+    int DX = 1000;
+    int dx = DX / layout->nX;
+    int dy = dx;
+    int DY = dy * layout->nY;
+
+    // XXX make the aspect-ratio match the image
+    if (layout->i == 0) {
+        KapaResize (kapa, DX, DY);
+    }
+
+    KapaClearPlots (kapa);
+    KapaInitGraph (&graphdata);
+    section.dx = dx / (float) DX;
+    section.dy = dy / (float) DY;
+    section.x = layout->iX * section.dx;
+    section.y = layout->iY * section.dy;
+    section.name = NULL;
+    psStringAppend (&section.name, "a%d", layout->i);
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    // examine sources to set data range
+    graphdata.xmin = -0.05;
+    graphdata.ymin = -0.05;
+    graphdata.xmax = +2.05;
+    graphdata.ymax = +2.05;
+    KapaSetLimits (kapa, &graphdata);
+
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "&ss&h_x| (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "&ss&h_y| (pixels)", KAPA_LABEL_YM);
+
+    psVector *xBright = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *yBright = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *xFaint  = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *yFaint  = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+
+    // construct the vectors
+    int nB = 0;
+    int nF = 0;
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+        if (source->moments == NULL)
+            continue;
+
+        xFaint->data.F32[nF] = source->moments->Sx;
+        yFaint->data.F32[nF] = source->moments->Sy;
+        nF++;
+
+        // XXX make this a user-defined cutoff
+        if (source->moments->SN < 50)
+            continue;
+
+        xBright->data.F32[nB] = source->moments->Sx;
+        yBright->data.F32[nB] = source->moments->Sy;
+        nB++;
+    }
+    xFaint->n = nF;
+    yFaint->n = nF;
+
+    xBright->n = nB;
+    yBright->n = nB;
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 0;
+    graphdata.size = 0.3;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, nF, &graphdata);
+    KapaPlotVector (kapa, nF, xFaint->data.F32, "x");
+    KapaPlotVector (kapa, nF, yFaint->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 2;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, nB, &graphdata);
+    KapaPlotVector (kapa, nB, xBright->data.F32, "x");
+    KapaPlotVector (kapa, nB, yBright->data.F32, "y");
+
+    if (layout->i == layout->nTotal - 1) {
+        psLogMsg ("psphot", 3, "saving plot to %s", file->filename);
+        KapaPNG (kapa, file->filename);
+	KapaClearPlots (kapa);
+    }
+
+    psFree (xBright);
+    psFree (yBright);
+    psFree (xFaint);
+    psFree (yFaint);
+
+    return true;
+}
+
+
+# else
+
+    bool pmSourcePlotMoments (const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout)
+{
+    psLogMsg ("psphot", 3, "skipping moments plot");
+    return true;
+}
+
+# endif
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlotPSFModel.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlotPSFModel.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlotPSFModel.c	(revision 15358)
@@ -0,0 +1,232 @@
+/** @file  pmSourcePlot.c
+ *
+ * This file contains functions to write source plots
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.11 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2006 IfA, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmResiduals.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmSourcePlots.h"
+#include "pmKapaPlots.h"
+
+// this variable is defined in psmodules.h if ohana-config is found
+# if (HAVE_KAPA)
+    # include <kapa.h>
+
+    // plot the sx, sy, sxy as vector field,
+    // plot the PSF measured sx, sy, sxy as vector field
+    // pull the sources from the config / file?
+    bool pmSourcePlotPSFModel (const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout)
+{
+
+    Graphdata graphdata;
+    KapaSection section;
+
+    psLogMsg ("psphot", 3, "creating psf model plot");
+
+    // find the currently selected readout
+    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
+
+    psArray *sources = psMetadataLookupPtr (NULL, readout->analysis, "PSPHOT.SOURCES");
+    if (sources == NULL)
+        return false;
+
+    int kapa = pmKapaOpen (false);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    int DX = 1000;
+    float dx = DX / layout->nX;
+    float dy = dx * layout->aspectRatio;
+    int DY = dy * layout->nY;
+
+    // XXX make the aspect-ratio match the image
+    if (layout->i == 0) {
+        KapaResize (kapa, DX, DY);
+    }
+    
+    KapaClearPlots (kapa);
+    KapaInitGraph (&graphdata);
+    section.dx = dx / DX;
+    section.dy = dy / DY;
+    section.x = layout->iX * section.dx;
+    section.y = layout->iY * section.dy;
+    section.name = NULL;
+    psStringAppend (&section.name, "a%d", layout->i);
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    psVector *xMNT = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+    psVector *yMNT = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+    psVector *xPSF = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+    psVector *yPSF = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+    psVector *xMIN = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+    psVector *yMIN = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+
+    // construct the plot vectors
+    int nMNT = 0;
+    int nPSF = 0;
+    int nMIN = 0;
+    dx = 0;
+    dy = 0;
+    float scale = 10;
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+        if (source->moments == NULL)
+            continue;
+        if (source->moments->SN < 25)
+            continue;
+        if (source->type != PM_SOURCE_TYPE_STAR)
+            continue;
+
+        pmModel *model = source->modelPSF;
+        if (model == NULL)
+            continue;
+
+        psF32 *PAR = model->params->data.F32;
+
+        psEllipseMoments moments;
+        moments.x2 = source->moments->Sx;
+        moments.y2 = source->moments->Sy;
+        moments.xy = source->moments->Sxy;
+
+        // force the axis ratio to be < 20.0
+        psEllipseAxes axes_mnt = psEllipseMomentsToAxes (moments, 20.0);
+        psEllipseAxes axes_psf = pmPSF_ModelToAxes (PAR, 20.0);
+
+        // moments major axis
+        dx = scale*axes_mnt.major*cos(axes_mnt.theta);
+        dy = scale*axes_mnt.major*sin(axes_mnt.theta);
+        xMNT->data.F32[nMNT] = PAR[PM_PAR_XPOS] - dx;
+        yMNT->data.F32[nMNT] = PAR[PM_PAR_YPOS] - dy;
+        nMNT++;
+        xMNT->data.F32[nMNT] = PAR[PM_PAR_XPOS] + dx;
+        yMNT->data.F32[nMNT] = PAR[PM_PAR_YPOS] + dy;
+        nMNT++;
+
+        // psf major axis
+        dx = scale*axes_psf.major*cos(axes_psf.theta);
+        dy = scale*axes_psf.major*sin(axes_psf.theta);
+        xPSF->data.F32[nPSF] = PAR[PM_PAR_XPOS] - dx;
+        yPSF->data.F32[nPSF] = PAR[PM_PAR_YPOS] - dy;
+        nPSF++;
+        xPSF->data.F32[nPSF] = PAR[PM_PAR_XPOS] + dx;
+        yPSF->data.F32[nPSF] = PAR[PM_PAR_YPOS] + dy;
+        nPSF++;
+
+        // minor axis (to show size)
+        dy = +scale*axes_psf.minor*cos(axes_psf.theta);
+        dx = -scale*axes_psf.minor*sin(axes_psf.theta);
+        xMIN->data.F32[nMIN] = PAR[PM_PAR_XPOS] - dx;
+        yMIN->data.F32[nMIN] = PAR[PM_PAR_YPOS] - dy;
+        nMIN++;
+        xMIN->data.F32[nMIN] = PAR[PM_PAR_XPOS] + dx;
+        yMIN->data.F32[nMIN] = PAR[PM_PAR_YPOS] + dy;
+        nMIN++;
+
+        graphdata.xmin = PS_MIN(graphdata.xmin, PAR[PM_PAR_XPOS]);
+        graphdata.xmax = PS_MAX(graphdata.xmax, PAR[PM_PAR_XPOS]);
+        graphdata.ymin = PS_MIN(graphdata.ymin, PAR[PM_PAR_YPOS]);
+        graphdata.ymax = PS_MAX(graphdata.ymax, PAR[PM_PAR_YPOS]);
+    }
+    xMNT->n = yMNT->n = nMNT;
+    xPSF->n = yPSF->n = nPSF;
+    xMIN->n = yMIN->n = nMIN;
+
+    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 set the plot range to match the image
+    KapaSetLimits (kapa, &graphdata);
+
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "x (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "y (pixels)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa, "vector is major axis (scaled by 20) : black are moments, blue are psf model, red is psf minor axis", KAPA_LABEL_XP);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 100;
+    graphdata.size = 0.3;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, nMNT, &graphdata);
+    KapaPlotVector (kapa, nMNT, xMNT->data.F32, "x");
+    KapaPlotVector (kapa, nMNT, yMNT->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 100;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, nPSF, &graphdata);
+    KapaPlotVector (kapa, nPSF, xPSF->data.F32, "x");
+    KapaPlotVector (kapa, nPSF, yPSF->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 100;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, nMIN, &graphdata);
+    KapaPlotVector (kapa, nMIN, xMIN->data.F32, "x");
+    KapaPlotVector (kapa, nMIN, yMIN->data.F32, "y");
+
+    if (layout->i == layout->nTotal - 1) {
+        psLogMsg ("psphot", 3, "saving plot to %s", file->filename);
+        KapaPNG (kapa, file->filename);
+	KapaClearPlots (kapa);
+    }
+
+    psFree (xMNT);
+    psFree (yMNT);
+    psFree (xPSF);
+    psFree (yPSF);
+    psFree (xMIN);
+    psFree (yMIN);
+
+    return true;
+}
+
+# else
+
+    bool pmSourcePlotPSFModel (const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout)
+{
+    psLogMsg ("psphot", 3, "skipping psf model plot");
+    return true;
+}
+
+# endif
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlots.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlots.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlots.c	(revision 15358)
@@ -0,0 +1,170 @@
+/** @file  pmSourcePlot.c
+ *
+ * This file contains functions to write source plots
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-05-04 00:54:09 $
+ *
+ *  Copyright 2006 IfA, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmConfig.h"
+#include "pmDetrendDB.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmSourcePlots.h"
+
+// this function is called for the specific plotting program at the fileLevel
+// fileLevel must be >= chip
+bool pmFPAviewWriteSourcePlot(const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    if ((view->readout != -1) || (view->cell != -1)) {
+        psError(PS_ERR_UNKNOWN, true, "source plots must have fileLevel >= chip");
+        return false;
+    }
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAWriteSourcePlot (fpa, view, file, config, NULL);
+        return true;
+    }
+
+    pmChipWriteSourcePlot (fpa, view, file, config, NULL);
+    return true;
+}
+
+// read in all chip-level SourcePlot files for this FPA
+bool pmFPAWriteSourcePlot (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    // if the layout is not defined, this level is the top level being plotted
+    // determine the plot layout
+    if (!layout) {
+        layout = pmSourcePlotLayoutAlloc ();
+        // XXX temporary hardwired values for essence?
+        layout->nX = 4;
+        layout->nY = 2;
+        layout->aspectRatio = 1.0;
+        // count the number of chips and their layout (xrange, yrange, axis ratio)
+        for (int i = 0; i < fpa->chips->n; i++) {
+            pmChip *chip = fpa->chips->data[i];
+            if (chip->data_exists) {
+                layout->nTotal ++;
+            }
+        }
+    }
+
+    pmFPAview *chipView = pmFPAviewAlloc(0);
+
+    while (pmFPAviewNextChip (chipView, fpa, 1) != NULL) {
+        pmChipWriteSourcePlot (fpa, chipView, file, config, layout);
+        layout->i ++;
+        layout->iX ++;
+        if (layout->iX == layout->nX) {
+            layout->iY++;
+            layout->iX = 0;
+        }
+    }
+    psFree (chipView);
+    psFree (layout);
+    return true;
+}
+
+// read in all cell-level SourcePlot files for this chip
+bool pmChipWriteSourcePlot (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout)
+{
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    // if the layout is not defined, this level is the top level being plotted
+    // determine the plot layout (always single chip)
+    if (!layout) {
+        layout = pmSourcePlotLayoutAlloc ();
+        layout->nTotal = 1;
+        layout->nX = 1;
+        layout->nY = 1;
+        layout->aspectRatio = 1.0;
+    } else {
+        psMemIncrRefCounter (layout);
+    }
+
+    pmFPAview *newView = pmFPAviewAlloc(0);
+    newView->chip = view->chip;
+
+    while (pmFPAviewNextCell (newView, fpa, 1) != NULL) {
+        while (pmFPAviewNextReadout (newView, fpa, 1) != NULL) {
+
+            if (!strcmp (file->name, "SOURCE.PLOT.PSFMODEL")) {
+                pmSourcePlotPSFModel (newView, file, config, layout);
+            }
+            if (!strcmp (file->name, "SOURCE.PLOT.MOMENTS")) {
+                pmSourcePlotMoments (newView, file, config, layout);
+            }
+            if (!strcmp (file->name, "SOURCE.PLOT.APRESID")) {
+                pmSourcePlotApResid (newView, file, config, layout);
+            }
+        }
+    }
+    psFree (newView);
+    psFree (layout);
+    return true;
+}
+
+static void pmSourcePlotLayoutFree(pmSourcePlotLayout *layout)
+{
+    return;
+}
+
+pmSourcePlotLayout *pmSourcePlotLayoutAlloc()
+{
+    pmSourcePlotLayout *layout = (pmSourcePlotLayout *)psAlloc(sizeof(pmSourcePlotLayout));
+    psMemSetDeallocator(layout, (psFreeFunc)pmSourcePlotLayoutFree );
+
+    layout->nX = layout->nY = layout->nTotal = 0;
+    layout->iX = layout->iY = layout->i  =  0;
+    layout->aspectRatio = 1.0;
+    return (layout);
+}
+
+/* we have three types of plots to produce (maybe more?)
+ * for each plot, we need to supply the following information:
+ *     - what are overall dimensions?
+ *     - what is the relative position of this element?
+ *     - what are the dimensions of this element?
+ *     - create a new page for this element?
+
+ * the answers to these questions come from slightly different locations
+ * for the different types of plots:
+
+ *  1) focal-plane layout based plots:
+ *     - dimensions depend on fileLevel
+
+ *  2) multi-row plots:
+ *     - dimensions depend on total number of rows
+
+ *  2) multi-page plots:
+ *     - dimensions depend on total number of rows
+ */
+
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlots.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlots.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourcePlots.h	(revision 15358)
@@ -0,0 +1,41 @@
+/* @file  pmSourcePlots.h
+ * @brief functions to create plots illustrating source properties
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-05-04 00:54:09 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_SOURCE_PLOTS_H
+#define PM_SOURCE_PLOTS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef struct
+{
+    int nX;
+    int nY;
+    int nTotal;
+    int iX;
+    int iY;
+    int i;
+    float aspectRatio;
+}
+pmSourcePlotLayout;
+
+// typedef bool (*pmSourcePlotFunction)(pmConfig *config, pmFPAview *view, pmSourcePlotLayout *layout);
+
+pmSourcePlotLayout *pmSourcePlotLayoutAlloc();
+
+bool pmFPAviewWriteSourcePlot(const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmFPAWriteSourcePlot (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout);
+bool pmChipWriteSourcePlot (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout);
+bool pmSourcePlotPSFModel (const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout);
+bool pmSourcePlotMoments (const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout);
+bool pmSourcePlotApResid (const pmFPAview *view, pmFPAfile *file, const pmConfig *config, pmSourcePlotLayout *layout);
+
+/// @}
+#endif // PM_SOURCE_PLOTS_H
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceSky.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceSky.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceSky.c	(revision 15358)
@@ -0,0 +1,150 @@
+/** @file  pmSourceSky.c
+ *
+ *  Functions to measure the local sky and sky variance for sources on images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.15 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAMaskWeight.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmSourceSky.h"
+
+/******************************************************************************
+pmSource *pmSourceLocalSky(source, statsOptions, Radius): this
+routine creates a new pmSource.moments element if needed and sets pmSource.pmMoments.sky
+
+The sky value is set from the pixels in the square annulus surrounding the
+peak pixel.
+
+The source.pixels and source.mask must already exist
+*****************************************************************************/
+
+bool pmSourceLocalSky(
+    pmSource *source,
+    psStatsOptions statsOptions,
+    psF32 Radius,
+    psMaskType maskVal,
+    psMaskType mark)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_IMAGE_NON_NULL(source->pixels, false);
+    PS_ASSERT_IMAGE_NON_NULL(source->maskObj, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_INT_POSITIVE(Radius, false);
+    PS_ASSERT_INT_NONNEGATIVE(Radius, false);
+
+    psStatsOptions statistic = psStatsSingleOption(statsOptions);
+    if (statistic == 0) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Multiple or no statistics specified: %x\n", statsOptions);
+        return NULL;
+    }
+
+    psImage *image = source->pixels;
+    psImage *mask  = source->maskObj;
+    pmPeak *peak  = source->peak;
+    psRegion srcRegion;
+
+    srcRegion = psRegionForSquare(peak->x, peak->y, Radius);
+    srcRegion = psRegionForImage(mask, srcRegion);
+
+    psImageMaskRegion(mask, srcRegion, "OR", mark);
+    psStats *myStats = psStatsAlloc(statsOptions);
+    if (!psImageStats(myStats, image, mask, maskVal | mark)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to get image statistics.\n");
+        psFree(myStats);
+        return false;
+    }
+    psImageMaskRegion(mask, srcRegion, "AND", PS_NOT_U8(mark));
+    double value = psStatsGetValue(myStats, statistic);
+    psFree(myStats);
+
+    if (isnan(value)) {
+        psTrace("psModules.objects", 3, "---- %s(false) end ----\n", __func__);
+        return(false);
+    }
+    if (source->moments == NULL) {
+        source->moments = pmMomentsAlloc();
+    }
+    source->moments->Sky = value;
+    psTrace("psModules.objects", 3, "---- %s(true) end ----\n", __func__);
+    return (true);
+}
+
+// A complementary function to pmSourceLocalSky: calculate the local median variance
+bool pmSourceLocalSkyVariance(
+    pmSource *source,
+    psStatsOptions statsOptions,
+    psF32 Radius,
+    psMaskType maskVal,
+    psMaskType mark
+)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_IMAGE_NON_NULL(source->weight, false);
+    PS_ASSERT_IMAGE_NON_NULL(source->maskObj, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_INT_POSITIVE(Radius, false);
+    PS_ASSERT_INT_NONNEGATIVE(Radius, false);
+
+    psStatsOptions statistic = psStatsSingleOption(statsOptions);
+    if (statistic == 0) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Multiple or no statistics specified: %x\n", statsOptions);
+        return NULL;
+    }
+
+    psImage *image = source->weight;
+    psImage *mask  = source->maskObj;
+    pmPeak *peak  = source->peak;
+    psRegion srcRegion;
+
+    srcRegion = psRegionForSquare(peak->x, peak->y, Radius);
+    srcRegion = psRegionForImage(mask, srcRegion);
+
+    psImageMaskRegion(mask, srcRegion, "OR", mark);
+    psStats *myStats = psStatsAlloc(statsOptions);
+    if (!psImageStats(myStats, image, mask, maskVal | mark)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to get image statistics.\n");
+        psFree(myStats);
+        return false;
+    }
+    psImageMaskRegion(mask, srcRegion, "AND", PS_NOT_U8(mark));
+    double value = psStatsGetValue(myStats, statistic);
+    psFree(myStats);
+
+    if (isnan(value)) {
+        psTrace("psModules.objects", 3, "---- %s(false) end ----\n", __func__);
+        return(false);
+    }
+    if (source->moments == NULL) {
+        source->moments = pmMomentsAlloc();
+    }
+    source->moments->dSky = value;
+    psTrace("psModules.objects", 3, "---- %s(true) end ----\n", __func__);
+    return (true);
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceSky.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceSky.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceSky.h	(revision 15358)
@@ -0,0 +1,47 @@
+/* @file  pmSourceSky.h
+ * @author EAM, IfA; GLG, MHPCC
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-06-20 02:22:26 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_SOURCE_SKY_H
+# define PM_SOURCE_SKY_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/** pmSourceLocalSky()
+ *
+ * Measure the local sky in the vicinity of the given source. The Radius
+ * defines the square aperture in which the moments will be measured. This
+ * function assumes the source pixels have been defined, and that the value of
+ * Radius here is smaller than the value of Radius used to define the pixels. The
+ * annular region not contained within the radius defined here is used to measure
+ * the local background in the vicinity of the source. The local background
+ * measurement uses the specified statistic passed in via the statsOptions entry.
+ * This function allocates the pmMoments structure. The resulting sky is used to
+ * set the value of the pmMoments.sky element of the provided pmSource structure.
+ *
+ */
+bool pmSourceLocalSky(
+    pmSource *source,   ///< The input image (float)
+    psStatsOptions statsOptions, ///< The statistic used in calculating the background sky
+    float Radius,   ///< The inner radius of the square annulus to exclude
+    psMaskType maskVal,                 ///< Value to mask
+    psMaskType mark                     ///< Mask value for marking
+);
+
+
+// A complementary function to pmSourceLocalSky: calculate the local sky variance
+bool pmSourceLocalSkyVariance(
+    pmSource *source,   ///< The input image (float)
+    psStatsOptions statsOptions, ///< The statistic used in calculating the background sky
+    float Radius,   ///< The inner radius of the square annulus to exclude
+    psMaskType maskVal,                 ///< Value to mask
+    psMaskType mark                     ///< Mask value for marking
+);
+
+/// @}
+# endif /* PM_SOURCE_PHOTOMETRY_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceUtils.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceUtils.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceUtils.c	(revision 15358)
@@ -0,0 +1,113 @@
+/** @file  pmSource.c
+ *
+ *  Functions to define and manipulate sources on images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-21 00:09:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAMaskWeight.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmResiduals.h"
+#include "pmGrowthCurve.h"
+#include "pmTrend2D.h"
+#include "pmPSF.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmSourceUtils.h"
+
+/******************************************************************************
+    pmSourceModelGuess(source, model): This function allocates a new
+    pmModel structure based on the given modelType specified in the argument list.
+    The corresponding pmModelGuess function is returned, and used to
+    supply the values of the params array in the pmModel structure.
+ 
+    XXX: Many parameters are based on the src->moments structure, which is in
+    image, not subImage coords.  Therefore, the calls to the model evaluation
+    functions will be in image, not subImage coords.  Remember this.
+*****************************************************************************/
+pmModel *pmSourceModelGuess(pmSource *source,
+                            pmModelType modelType)
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, NULL);
+    PS_ASSERT_PTR_NON_NULL(source->moments, NULL);
+    PS_ASSERT_PTR_NON_NULL(source->peak, NULL);
+
+    pmModel *model = pmModelAlloc(modelType);
+
+    if (!model->modelGuess(model, source)) {
+	psFree (model);
+	return NULL;
+    }
+
+    psTrace("psModules.objects", 3, "---- %s() end ----\n", __func__);
+    return(model);
+}
+
+pmSource *pmSourceFromModel (pmModel *model, pmReadout *readout, float radius, pmSourceType type) {
+
+    pmSource *source = pmSourceAlloc ();
+
+    // use the model centroid for the peak
+    switch (type) {
+      case PM_SOURCE_TYPE_STAR:
+	source->modelPSF = model;
+	break;
+      case PM_SOURCE_TYPE_EXTENDED:
+	source->modelEXT = model;
+	break;
+      default:
+	psAbort ("invalid source type");
+    }
+    source->type = type;
+
+    pmCell *cell = readout->parent;
+
+    float Io    = model->params->data.F32[PM_PAR_I0];
+    float xChip = model->params->data.F32[PM_PAR_XPOS];
+    float yChip = model->params->data.F32[PM_PAR_YPOS];
+
+    source->peak = pmPeakAlloc (xChip, yChip, Io, PM_PEAK_LONE);
+
+    int x0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.X0");
+    int y0Cell = psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0");
+    int xParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY");
+    int yParityCell = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY");
+
+    // XXX fix the binning : currently not selected from concepts
+    // int xBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN"); // Binning in x and y
+    // int yBin = psMetadataLookupS32(NULL, cell->concepts, "CELL.YBIN"); // Binning in x and y
+    int xBin = 1;
+    int yBin = 1;
+
+    // Position on the cell 
+    float xCell = PM_CHIP_TO_CELL(xChip, x0Cell, xParityCell, xBin);
+    float yCell = PM_CHIP_TO_CELL(yChip, y0Cell, yParityCell, yBin);
+
+    // Position on the readout
+    // float xReadout = CELL_TO_READOUT(xCell, x0Readout);
+    // float yReadout = CELL_TO_READOUT(yCell, y0Readout);
+    
+    pmSourceDefinePixels (source, readout, xCell, yCell, radius);
+
+    return (source);
+}
+
Index: /branches/eam_branch_20071023/psModules/src/objects/pmSourceUtils.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmSourceUtils.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmSourceUtils.h	(revision 15358)
@@ -0,0 +1,40 @@
+/* @file  pmSourceUtils.h
+ *
+ * Utility functions for working with pmSources
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-08-24 00:11:02 $
+ * Copyright 2007 IfA, University of Hawaii
+ */
+
+# ifndef PM_SOURCE_UTILS_H
+# define PM_SOURCE_UTILS_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+/** pmSourceModelGuess()
+ *
+ * Convert available data to an initial guess for the given model. This
+ * function allocates a pmModel entry for the pmSource structure based on the
+ * provided model selection. The method of defining the model parameter guesses
+ * are specified for each model below. The guess values are placed in the model
+ * parameters. The function returns TRUE on success or FALSE on failure.
+ *
+ */
+pmModel *pmSourceModelGuess(
+    pmSource *source,   ///< The input pmSource
+    pmModelType model   ///< The type of model to be created.
+);
+
+pmSource *pmSourceFromModel (
+  pmModel *model, 
+  pmReadout *readout, 
+  float radius,
+  pmSourceType type
+  );
+
+/// @}
+# endif /* PM_MODEL_CLASS_H */
Index: /branches/eam_branch_20071023/psModules/src/objects/pmTrend2D.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmTrend2D.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmTrend2D.c	(revision 15358)
@@ -0,0 +1,287 @@
+/** @file  pmTrend2D.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-10-09 19:26:25 $
+ *
+ *  Copyright 2004 Institute for Astronomy, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+# include <strings.h>
+# include <pslib.h>
+# include "pmTrend2D.h"
+
+static void pmTrend2DFree (pmTrend2D *trend) {
+
+    if (trend == NULL) 
+        return;
+
+    psFree (trend->stats);
+    psFree (trend->poly);
+    psFree (trend->map);
+    return;
+}
+
+pmTrend2D *pmTrend2DAlloc (pmTrend2DMode mode, psImage *image, int nXtrend, int nYtrend, psStats *stats)
+{
+    assert (image);
+
+    pmTrend2D *trend = (pmTrend2D *) psAlloc(sizeof(pmTrend2D));
+    psMemSetDeallocator(trend, (psFreeFunc) pmTrend2DFree);
+
+    trend->map = NULL;
+    trend->poly = NULL;
+    trend->stats = psMemIncrRefCounter (stats);
+    trend->mode = mode;
+	
+    switch (mode) {
+      case PM_TREND_POLY_ORD:
+	trend->poly = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, nXtrend, nYtrend);
+	// set masking somehow
+ 	for (int nx = 0; nx < trend->poly->nX + 1; nx++) {
+	    for (int ny = 0; ny < trend->poly->nY + 1; ny++) {
+		if (nx + ny >= PS_MAX (trend->poly->nX, trend->poly->nY) + 1) {
+		    trend->poly->coeffMask[nx][ny] = PS_POLY_MASK_SET;
+		} else {
+		    trend->poly->coeffMask[nx][ny] = PS_POLY_MASK_NONE;
+		}
+	    }
+	}
+	break;
+
+      case PM_TREND_POLY_CHEB:
+	trend->poly = psPolynomial2DAlloc (PS_POLYNOMIAL_CHEB, nXtrend, nYtrend);
+	break;
+
+      case PM_TREND_MAP: {
+	  // binning defines the map scale relationship
+	  psImageBinning *binning = psImageBinningAlloc();
+	  binning->nXruff = nXtrend;
+	  binning->nYruff = nYtrend;
+	  binning->nXfine = image->numCols;
+	  binning->nYfine = image->numRows;
+
+	  trend->map = psImageMapAlloc (image, binning, stats);
+	  psFree (binning);
+	  break;
+      }
+
+      default:
+	psAbort ("error");
+    }
+    return (trend);
+}
+
+pmTrend2D *pmTrend2DNoImageAlloc (pmTrend2DMode mode, psImageBinning *binning, psStats *stats)
+{
+    pmTrend2D *trend = (pmTrend2D *) psAlloc(sizeof(pmTrend2D));
+    psMemSetDeallocator(trend, (psFreeFunc) pmTrend2DFree);
+
+    trend->map = NULL;
+    trend->poly = NULL;
+    trend->stats = psMemIncrRefCounter (stats);
+    trend->mode = mode;
+	
+    switch (mode) {
+      case PM_TREND_POLY_ORD:
+	trend->poly = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, binning->nXruff, binning->nYruff);
+	// set masking somehow
+ 	for (int nx = 0; nx < trend->poly->nX + 1; nx++) {
+	    for (int ny = 0; ny < trend->poly->nY + 1; ny++) {
+		if (nx + ny >= PS_MAX (trend->poly->nX, trend->poly->nY) + 1) {
+		    trend->poly->coeffMask[nx][ny] = PS_POLY_MASK_SET;
+		} else {
+		    trend->poly->coeffMask[nx][ny] = PS_POLY_MASK_NONE;
+		}
+	    }
+	}
+	break;
+
+      case PM_TREND_POLY_CHEB:
+	trend->poly = psPolynomial2DAlloc (PS_POLYNOMIAL_CHEB, binning->nXruff, binning->nYruff);
+	break;
+
+      case PM_TREND_MAP: {
+	  // binning defines the map scale relationship
+	  trend->map = psImageMapNoImageAlloc (binning, stats);
+	  break;
+      }
+
+      default:
+	psAbort ("error");
+    }
+    return (trend);
+}
+
+pmTrend2D *pmTrend2DFieldAlloc (pmTrend2DMode mode, int nXfield, int nYfield, int nXtrend, int nYtrend, psStats *stats)
+{
+    pmTrend2D *trend = (pmTrend2D *) psAlloc(sizeof(pmTrend2D));
+    psMemSetDeallocator(trend, (psFreeFunc) pmTrend2DFree);
+
+    trend->map = NULL;
+    trend->poly = NULL;
+    trend->stats = psMemIncrRefCounter (stats);
+    trend->mode = mode;
+	
+    switch (mode) {
+      case PM_TREND_POLY_ORD:
+	trend->poly = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, nXtrend, nYtrend);
+	// set masking somehow
+ 	for (int nx = 0; nx < trend->poly->nX + 1; nx++) {
+	    for (int ny = 0; ny < trend->poly->nY + 1; ny++) {
+		if (nx + ny >= PS_MAX (trend->poly->nX, trend->poly->nY) + 1) {
+		    trend->poly->coeffMask[nx][ny] = PS_POLY_MASK_SET;
+		} else {
+		    trend->poly->coeffMask[nx][ny] = PS_POLY_MASK_NONE;
+		}
+	    }
+	}
+	break;
+
+      case PM_TREND_POLY_CHEB:
+	trend->poly = psPolynomial2DAlloc (PS_POLYNOMIAL_CHEB, nXtrend, nYtrend);
+	break;
+
+      case PM_TREND_MAP: {
+	  // binning defines the map scale relationship
+	  psImageBinning *binning = psImageBinningAlloc();
+	  binning->nXfine = nXfield;
+	  binning->nYfine = nYfield;
+	  binning->nXruff = nXtrend;
+	  binning->nYruff = nYtrend;
+
+	  trend->map = psImageMapAlloc (NULL, binning, stats);
+	  psFree (binning);
+	  break;
+      }
+
+      default:
+	psAbort ("error");
+    }
+    return (trend);
+}
+
+bool pmTrend2DFit (pmTrend2D *trend, psVector *mask, psMaskType maskVal, psVector *x, psVector *y, psVector *f, psVector *df) {
+
+    bool status;
+
+    assert (trend);
+    assert (x);
+    assert (y);
+    assert (f);
+
+    switch (trend->mode) {
+      case PM_TREND_POLY_ORD:
+      case PM_TREND_POLY_CHEB:
+        status = psVectorClipFitPolynomial2D (trend->poly, trend->stats, mask, maskVal, f, df, x, y);
+	// we can use the API here which adjusts the polynomial order based on the number
+	// of points in the image, and potentially based on the fractional range of the
+	// data?
+	break;
+
+      case PM_TREND_MAP:
+	// XXX supply fraction from trend elements
+	// XXX need to add the API which adjusts the scale
+	status = psImageMapClipFit (trend->map, trend->stats, mask, maskVal, x, y, f, df);
+	break;
+
+      default:
+	psAbort ("error");
+    }
+    return status;
+}
+
+double pmTrend2DEval (pmTrend2D *trend, float x, float y) {
+
+    double result;
+
+    assert (trend);
+
+    switch (trend->mode) {
+      case PM_TREND_POLY_ORD:
+      case PM_TREND_POLY_CHEB:
+	result = psPolynomial2DEval (trend->poly, x, y);
+	break;
+
+      case PM_TREND_MAP:
+	result = psImageMapEval (trend->map, x, y);
+	break;
+
+      default:
+	psAbort ("error");
+    }
+    return result;
+}
+
+psVector *pmTrend2DEvalVector (pmTrend2D *trend, psVector *x, psVector *y) {
+
+    psVector *result;
+
+    assert (trend);
+    assert (x);
+    assert (y);
+
+    switch (trend->mode) {
+      case PM_TREND_POLY_ORD:
+      case PM_TREND_POLY_CHEB:
+	result = psPolynomial2DEvalVector (trend->poly, x, y);
+	break;
+
+      case PM_TREND_MAP:
+	result = psImageMapEvalVector (trend->map, x, y);
+	break;
+
+      default:
+	psAbort ("error");
+    }
+    return result;
+}
+
+psString pmTrend2DModeToString (pmTrend2DMode mode) {
+    
+    psString name;
+
+    switch (mode) {
+      case PM_TREND_NONE:
+	name = psStringCopy ("NONE");
+	break;
+      case PM_TREND_POLY_ORD:
+	name = psStringCopy ("POLY_ORD");
+	break;
+      case PM_TREND_POLY_CHEB:
+	name = psStringCopy ("POLY_CHEB");
+	break;
+      case PM_TREND_MAP:
+	name = psStringCopy ("MAP");
+	break;
+      default:
+	psAbort ("invalid mode %d", mode);
+    }
+    return name;
+}
+
+pmTrend2DMode pmTrend2DModeFromString (psString name) {
+
+    if (!name) return PM_TREND_NONE;
+
+    if (!strcasecmp (name, "NONE")) {
+	return PM_TREND_NONE;
+    }
+    if (!strcasecmp (name, "POLY_ORD")) {
+	return PM_TREND_POLY_ORD;
+    }
+    if (!strcasecmp (name, "POLY_CHEB")) {
+	return PM_TREND_POLY_CHEB;
+    }
+    if (!strcasecmp (name, "MAP")) {
+	return PM_TREND_MAP;
+    }
+    psError (PS_ERR_UNKNOWN, true, "Unknown pmTrend2D mode %s\n", name);
+    return PM_TREND_NONE;
+}
Index: /branches/eam_branch_20071023/psModules/src/objects/pmTrend2D.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/objects/pmTrend2D.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/objects/pmTrend2D.h	(revision 15358)
@@ -0,0 +1,50 @@
+/* @file  pmTrend2D.h
+ *
+ * functions to represent ways of modeling a 2D trend
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-09-24 21:27:58 $
+ * Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+# ifndef PM_TREND_2D_H
+# define PM_TREND_2d_H
+
+/// @addtogroup Objects Object Detection / Analysis Functions
+/// @{
+
+typedef enum {
+    PM_TREND_NONE,
+    PM_TREND_POLY_ORD,
+    PM_TREND_POLY_CHEB,
+    PM_TREND_MAP,
+} pmTrend2DMode;
+
+typedef struct {
+    psPolynomial2D *poly;
+    psImageMap *map;
+    psStats *stats;
+    pmTrend2DMode mode; // POLY_ORD, POLY_CHEB, MAP
+} pmTrend2D;
+
+// allocate a pmTrend2D structure tied to an image dimensions.  nXtrend,nYtrend is the order for the polynomials, max number of grid cells for
+// psImageMap
+pmTrend2D *pmTrend2DAlloc (pmTrend2DMode mode, psImage *image, int nXtrend, int nYtrend, psStats *stats);
+
+pmTrend2D *pmTrend2DNoImageAlloc (pmTrend2DMode mode, psImageBinning *binning, psStats *stats);
+
+// allocate a pmTrend2D tied to an abstract field with size nXfield,nYfield 
+pmTrend2D *pmTrend2DFieldAlloc (pmTrend2DMode mode, int nXfield, int nYfield, int nXtrend, int nYtrend, psStats *stats);
+
+bool pmTrend2DFit (pmTrend2D *trend, psVector *mask, psMaskType maskVal, psVector *x, psVector *y, psVector *f, psVector *df);
+
+double pmTrend2DEval (pmTrend2D *trend, float x, float y);
+psVector *pmTrend2DEvalVector (pmTrend2D *trend, psVector *x, psVector *y);
+
+psString pmTrend2DModeToString (pmTrend2DMode mode);
+pmTrend2DMode pmTrend2DModeFromString (psString name);
+
+/// @}
+# endif
Index: /branches/eam_branch_20071023/psModules/src/psmodules.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/psmodules.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/psmodules.h	(revision 15358)
@@ -0,0 +1,108 @@
+#ifndef PS_MODULES_H
+#define PS_MODULES_H
+
+#include <pslib.h>
+
+// the following headers are from psModule:extras
+#include <psPipe.h>
+#include <psIOBuffer.h>
+#include <psVectorBracket.h>
+#include <pmKapaPlots.h>
+
+// XXX the following headers define constructs needed by the elements below
+#include <pmConfig.h>
+#include <pmDetrendDB.h>
+#include <pmHDU.h>
+#include <pmFPA.h>
+#include <pmFPALevel.h>
+#include <pmFPAview.h>
+#include <pmFPAfile.h>
+
+// the following headers are from psModule:config
+#include <pmErrorCodes.h>
+#include <pmConfigRecipes.h>
+#include <pmConfigCamera.h>
+#include <pmConfigCommand.h>
+#include <pmConfigMask.h>
+#include <pmVersion.h>
+
+// the following headers are from psModule:concepts
+#include <pmConcepts.h>
+#include <pmConceptsRead.h>
+#include <pmConceptsStandard.h>
+#include <pmConceptsWrite.h>
+#include <pmConceptsPhotcode.h>
+#include <pmConceptsAverage.h>
+#include <pmConceptsUpdate.h>
+
+// the following headers are from psModule:camera
+#include <pmHDUUtils.h>
+#include <pmHDUGenerate.h>
+#include <pmFPAFlags.h>
+#include <pmFPAfileDefine.h>
+#include <pmFPAfileFitsIO.h>
+#include <pmFPAfileIO.h>
+#include <pmFPARead.h>
+#include <pmFPAConstruct.h>
+#include <pmFPACopy.h>
+#include <pmFPAHeader.h>
+#include <pmFPAMaskWeight.h>
+#include <pmFPAMosaic.h>
+#include <pmFPARead.h>
+#include <pmFPAWrite.h>
+#include <pmFPA_JPEG.h>
+#include <pmFPAExtent.h>
+#include <pmFPACalibration.h>
+#include <pmReadoutStack.h>
+#include <pmFPAUtils.h>
+#include <pmCellSquish.h>
+
+// the following headers are from psModule:detrend
+#include <pmFlatField.h>
+#include <pmFlatNormalize.h>
+#include <pmFringeStats.h>
+#include <pmMaskBadPixels.h>
+#include <pmNonLinear.h>
+#include <pmOverscan.h>
+#include <pmBias.h>
+#include <pmShutterCorrection.h>
+// #include <pmSkySubtract.h>
+
+// the following headers are from psModule:astrom
+#include <pmAstrometryWCS.h>
+#include <pmAstrometryObjects.h>
+#include <pmAstrometryDistortion.h>
+
+// the following headers are from psModule:imcombine
+#include <pmStack.h>
+#include <pmStackReject.h>
+#include <pmSubtraction.h>
+#include <pmSubtractionStamps.h>
+#include <pmSubtractionKernels.h>
+#include <pmSubtractionMatch.h>
+#include <pmSubtractionParams.h>
+#include <pmReadoutCombine.h>
+
+// the following headers are from psModule:objects
+#include <pmPeaks.h>
+#include <pmMoments.h>
+#include <pmResiduals.h>
+#include <pmGrowthCurve.h>
+#include <pmTrend2D.h>
+#include <pmPSF.h>
+#include <pmModel.h>
+#include <pmSource.h>
+#include <pmSourceUtils.h>
+#include <pmSourceIO.h>
+#include <pmSourceSky.h>
+#include <pmSourceFitModel.h>
+#include <pmSourceFitSet.h>
+#include <pmSourceContour.h>
+#include <pmSourcePlots.h>
+#include <pmPSF_IO.h>
+#include <pmPSFtry.h>
+#include <pmModelClass.h>
+#include <pmModelUtils.h>
+#include <pmSourcePhotometry.h>
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/sky/pmSkyCell.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/sky/pmSkyCell.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/sky/pmSkyCell.h	(revision 15358)
@@ -0,0 +1,18 @@
+#ifndef PM_SKY_CELL_H
+#define PM_SKY_CELL_H
+
+/// Structure representing a sky cell
+typedef struct
+{
+    psImage *image;                     // Image; type F32
+    psImage *mask;                      // Mask image; type MASK_TYPE
+    psImage *weight;                    // Weight map; type F32
+    psPlaneTransform *toTPA;            // Transformation from pixels to tangent plane
+    psPlaneTransform *fromTPA;          // Transformation from tangent plane to pixels
+    psProjection *toSky;                // Projection to the sky
+}
+pmSkyCell;
+
+
+
+#endif
Index: /branches/eam_branch_20071023/psModules/src/sky/pmWarp.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/sky/pmWarp.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/sky/pmWarp.h	(revision 15358)
@@ -0,0 +1,24 @@
+#ifndef PM_WARP_MAP_H
+#define PM_WARP_MAP_H
+
+// a single pswarpMap converts coordinates from one image to a second image
+// the linear model is only valid over a limited range of pixels
+typedef struct
+{
+    double Xo, Xx, Xy;
+    double Yo, Yx, Yy;
+    int xo;
+    int yo;
+}
+pswarpMap;
+
+// the pswarpMapGrid carries a collection of pswarpMag structures representing the
+// local value of the pswarpMap at different locations in the image.
+typedef struct
+{
+    pswarpMap ***maps;
+    int nXpts, nYpts;                   // number of x,y samples in the grid
+    int nXpix, nYpix;                   // x,y spacing in src image pixels of grid samples
+    int xMin,  yMin;                    // coordinate of first grid sample
+}
+pswarpMapGrid;
Index: /branches/eam_branch_20071023/psModules/src/sky/pmWarpMap.c
===================================================================
--- /branches/eam_branch_20071023/psModules/src/sky/pmWarpMap.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/sky/pmWarpMap.c	(revision 15358)
@@ -0,0 +1,241 @@
+#include <stdio.h>
+#include <math.h>
+#include <pslib.h>
+
+#include "pmWarpMap.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Allocators and deallocators
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static void warpMapFree(pswarpMap *map)
+{
+    // No operations required; this function exists merely to identify a pmWarpMap
+    return;
+}
+
+pswarpMap *pswarpMapAlloc(void)
+{
+    pswarpMap *map = (pswarpMap *) psAlloc (sizeof(pswarpMap));
+    psMemSetDeallocator(map, (psFreeFunc)warpMapFree);
+
+    map->Xo = map->Xx = map->Xy = NAN;
+    map->Yo = map->Yx = map->Yy = NAN;
+    map->xo = map->yo = NAN;
+
+    return map;
+}
+
+
+static void warpMapGridFree(pswarpMapGrid *grid)
+{
+    if (!grid->maps) {
+        // Nothing to free
+        return;
+    }
+
+    for (int i = 0; i < grid->nXpts; i++) {
+        for (int j = 0; j < grid->nYpts; j++) {
+            psFree(grid->maps[i][j]);
+        }
+        psFree(grid->maps[i]);
+    }
+    psFree (grid->maps);
+    return;
+}
+
+pswarpMapGrid *pswarpMapGridAlloc(int nXpts, int nYpts)
+{
+    PS_ASSERT_INT_POSITIVE(nXpts, NULL);
+    PS_ASSERT_INT_POSITIVE(nYpts, NULL);
+
+    pswarpMapGrid *grid = (pswarpMapGrid *) psAlloc(sizeof(pswarpMapGrid));
+    psMemSetDeallocator(grid, (psFreeFunc)warpMapGridFree);
+
+    grid->maps = psAlloc(nXpts*sizeof(void **));
+    for (int i = 0; i < nXpts; i++) {
+        grid->maps[i] = psAlloc(nYpts*sizeof(void *));
+        for (int j = 0; j < nYpts; j++) {
+            grid->maps[i][j] = pswarpMapAlloc();
+        }
+    }
+    grid->nXpts = nXpts;
+    grid->nYpts = nYpts;
+
+    grid->nXpix = 0;
+    grid->nYpix = 0;
+
+    return grid;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Operations
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// XXX for the moment, ignore readout->cell->chip offsets
+pswarpMapGrid *pswarpMapGridFromImage(pmSkyCell *dest, const pmReadout *src, int nXpix, int nYpix)
+{
+    PS_ASSERT_PTR_NON_NULL(dest, NULL);
+    PS_ASSERT_PTR_NON_NULL(src, NULL);
+    PS_ASSERT_IMAGE_NON_NULL(src->image, NULL);
+    PS_ASSERT_INT_NON_NEGATIVE(nXpix, NULL);
+    PS_ASSERT_INT_NON_NEGATIVE(nYpix, NULL);
+
+    // split the difference of the remainder
+    int xMin = 0.5 * (src->image->numCols % nXpix); // Minimum in x
+    int yMin = 0.5 * (src->image->numRows % nYpix); // Minimum in y
+
+    int nXpts = src->image->numCols / nXpix; // Number of points in x
+    if (src->image->numCols % nXpix == 0) {
+        nXpts++;
+    }
+    int nYpts = src->image->numRows / nYpix; // Number of points in y
+    if (src->image->numRows % nYpix == 0) {
+        nYpts++;
+    }
+
+    pmWarpMapGrid *grid = pmWarpMapGridAlloc(nXpts, nYpts);
+    grid->nXpix = nXpix;
+    grid->nYpix = nYpix;
+    grid->xMin = xMin;
+    grid->yMin = yMin;
+
+    for (int ni = 0, i = xMin; ni < nXpts; i += nXpix, ni++) {
+        for (int nj = 0, j = yMin; nj < nYpts; j += nYpix, nj++) {
+            pmWarpMapSetLocalModel(grid->maps[ni][nj], dest, src, i, j);
+        }
+    }
+
+    return grid;
+}
+
+bool pmWarpMapGridSetGrid(const pmWarpMapGrid *grid, int ix, int iy, int *gridX, int *gridY)
+{
+    *gridX = (ix - grid->xMin + 0.5*grid->nXpix) / grid->nXpix;
+    *gridY = (iy - grid->yMin + 0.5*grid->nYpix) / grid->nYpix;
+
+    return true;
+}
+
+bool pmWarpMapGridNextGrid(const pmWarpMapGrid *grid, int gridX, int gridY, int *nextX, int *nextY)
+{
+    *nextX = gridX*grid->nXpix + grid->xMin + 0.5*grid->nXpix;
+    *nextY = gridY*grid->nYpix + grid->yMin + 0.5*grid->nYpix;
+
+    return true;
+}
+
+// measure the max error accumulated in appling one grid point to its neighbors
+double pmWarpMapGridMaxError(const pmWarpMapGrid *grid)
+{
+    double maxError = 0;                // Maximum error
+
+    for (int i = 0; i < grid->nXpts - 1; i++) {
+        for (int j = 0; j < grid->nYpts - 1; j++) {
+            // measure the output coordinates for the next grid position using the current grid map
+            // compare with the coordinates measured using the next grid map
+            double xRaw, yRaw;          // Raw position
+            double xRef, yRef;          // Reference position
+
+            pmWarpMapApply(&xRaw, &yRaw, grid->maps[i][j], grid->maps[i][j]->xo + grid->nXpix,
+                           grid->maps[i][j]->yo);
+            pmWarpMapApply(&xRef, &yRef, grid->maps[i+1][j], grid->maps[i][j]->xo + grid->nXpix,
+                           grid->maps[i][j]->yo);
+
+            double posError = hypot(xRaw-xRef, yRaw-yRef);
+            maxError = PS_MAX(maxError, posError);
+        }
+    }
+
+    return maxError;
+}
+
+bool pmWarpMapApply(double *outX, double *outY, const pmWarpMap *map, double inX, double inY)
+{
+    *outX = map->Xo + map->Xx*inX + map->Xy*inY;
+    *outY = map->Yo + map->Yx*inX + map->Yy*inY;
+
+    return true;
+}
+
+bool pmWarpMapSetLocalModel(pmWarpMap *map, const pmSkyCell *dest, const pmReadout *src, int ix, int iy)
+{
+    PS_ASSERT_PTR_NON_NULL(map, false);
+    PS_ASSERT_PTR_NON_NULL(dest, false);
+    PS_ASSERT_PTR_NON_NULL(dest->toSky, false);
+    PS_ASSERT_PTR_NON_NULL(dest->fromTPA, false);
+    PS_ASSERT_PTR_NON_NULL(src, false);
+    pmCell *cell = src->parent;         // Parent cell of source
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    pmChip *chip = cell->parent;        // Parent chip of source
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->toFPA, false);
+    pmFPA *fpa = chip->parent;          // Parent FPA of source
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fpa->toTPA, false);
+    PS_ASSERT_PTR_NON_NULL(fpa->toSky, false);
+    PS_ASSERT_INT_NON_NEGATIVE(ix, false);
+    PS_ASSERT_INT_NON_NEGATIVE(iy, false);
+
+    // These will get allocated in the course of the calculations; may as well allocate them here
+    // XXX save these as static for speed?
+    // Alternatively, pass in a buffer containing these pre-allocated.
+    psPlane *offset = psPlaneAlloc();   // Position offset from the main position of interest (ix,iy)
+    psPlane *FP = psPlaneAlloc();       // Position in the focal plane
+    psPlane *TP = psPlaneAlloc();       // Position in the tangent plane
+    psSphere *sky = psSphereAlloc();    // Position on the sky
+
+    // XXX need to include readout->cell->chip offsets
+    // --> Look up CELL.X0, CELL.Y0 in concepts
+    // --> Use readout->col0, readout->row0
+
+    // V(0,0) position
+    offset->x = ix;
+    offset->y = iy;
+    psPlaneTransformApply(FP, chip->toFPA, offset);
+    psPlaneTransformApply (TP, fpa->toTPA, FP);
+    psDeproject(sky, TP, fpa->toSky);
+    psProject(TP, sky, dest->toSky);
+    psPlane *V00 = psPlaneTransformApply(NULL, dest->fromTPA, TP); // Coordinates at the position of interest
+
+    // V(1,0) position
+    offset->x = ix + 1;
+    offset->y = iy;
+    psPlaneTransformApply(FP, chipSrc->toFPA, offset);
+    psPlaneTransformApply(TP, fpaSrc->toTPA, FP);
+    psDeproject(sky, TP, fpaSrc->toSky);
+    psProject(TP, sky, dest->toSky);
+    psPlane *V10 = psPlaneTransformApply(NULL, dest->fromTPA, TP); // Coordinates at position + (1,0)
+
+    // V(0,1) position
+    offset->x = ix;
+    offset->y = iy + 1;
+    psPlaneTransformApply(FP, chipSrc->toFPA, offset);
+    psPlaneTransformApply(TP, fpaSrc->toTPA, FP);
+    psDeproject(sky, TP, fpaSrc->toSky);
+    psProject(TP, sky, fpaDest->toSky);
+    psPlane *V01 = psPlaneTransformApply(NULL, dest->fromTPA, TP); // Coordinates at position + (0,1)
+
+    psFree(offset);
+    psFree(FP);
+    psFree(TP);
+    psFree(sky);
+
+    // Generate local linear transformation
+    map->Xx = V10->x - V00->x;
+    map->Xy = V01->x - V00->x;
+    map->Xo = V00->x - map->Xx * ix - map->Xy * iy;
+
+    map->Yx = V10->y - V00->y;
+    map->Yy = V01->y - V00->y;
+    map->Yo = V00->y - map->Yx * ix - map->Yy * iy;
+
+    map->xo = ix;
+    map->yo = iy;
+
+    psFree(V00);
+    psFree(V10);
+    psFree(V01);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/src/sky/pmWarpMap.h
===================================================================
--- /branches/eam_branch_20071023/psModules/src/sky/pmWarpMap.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/src/sky/pmWarpMap.h	(revision 15358)
@@ -0,0 +1,88 @@
+/*  @file pmWarpMap.h
+ *  @brief Warp Map Manipulation functions
+ * 
+ *  @author Paul Price, IfA
+ *  @author Eugene Magnier, IfA
+ * 
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-01-24 01:05:41 $
+ *  Copyright 2005-2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_WARP_MAP_H
+#define PM_WARP_MAP_H
+
+/// @addtogroup Sky
+/// @{
+
+// a single pmWarpMap converts coordinates from one image to a second image
+// the linear model is only valid over a limited range of pixels
+typedef struct
+{
+    double Xo, Xx, Xy;                  // Transformation coefficients for x
+    double Yo, Yx, Yy;                  // Transformation coefficients for y
+    int xo, yo;                         // Origin
+}
+pmWarpMap;
+
+// the pmWarpMapGrid carries a collection of pmWarpMag structures representing the
+// local value of the pmWarpMap at different locations in the image.
+typedef struct
+{
+    pmWarpMap ***maps;                  // 2D image of pointers to maps
+    int nXpts, nYpts;                   // number of x,y samples in the grid
+    int nXpix, nYpix;                   // x,y spacing in src image pixels of grid samples
+    int xMin,  yMin;                    // coordinate of first grid sample
+}
+pmWarpMapGrid;
+
+/// Allocator
+pmWarpMap *pmWarpMapAlloc(void);
+
+/// Allocator
+pmWarpMapGrid *pmWarpMapGridAlloc(int Nx, // Number of grid points in x
+                                  int Ny // Number of grid points in y
+                                 );
+
+
+// Operations follow
+
+
+// construct a grid with superpixel spacing of nXpix, nYpix
+pmWarpMapGrid *pmWarpMapGridFromImage(pmSkyCell *dest, // Destination sky cell
+                                      const pmReadout *src, // Source readout
+                                      int nXpix, // Number of pixels in x
+                                      int nYpix // Number of pixels in y
+                                     );
+
+bool pmWarpMapGridSetGrid(pmWarpMapGrid *grid,
+                          int ix, int iy,
+                          int *gridX,
+                          int *gridY
+                         );
+
+bool pmWarpMapGridNextGrid(pmWarpMapGrid *grid,
+                           int gridX,
+                           int gridY,
+                           int *nextX,
+                           int *nextY
+                          );
+
+// measure the max error accumulated in appling one grid point to its neighbors
+double pmWarpMapGridMaxError(const pmWarpMapGrid *grid
+                            );
+
+bool pmWarpMapApply(double *outX,
+                    double *outY,
+                    const pmWarpMap *map,
+                    double inX,
+                    double inY);
+
+// determine the map for the given pixel from src to dest. pixel is in src coords
+bool pmWarpMapSetLocalModel(pmWarpMap *map,
+                            const pmSkyCell *dest,
+                            const pmReadout *src,
+                            int ix, int iy
+                           );
+/// @}
+#endif
Index: /branches/eam_branch_20071023/psModules/templates/c
===================================================================
--- /branches/eam_branch_20071023/psModules/templates/c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/templates/c	(revision 15358)
@@ -0,0 +1,11 @@
+/** @file  filename.c
+ *
+ *  @brief Contains the definitions for ...
+ *
+ *  @author Robert DeSonia, MHPCC
+ *
+ *  @version $Revision: 1.1.1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2004-09-10 19:01:33 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
Index: /branches/eam_branch_20071023/psModules/templates/h
===================================================================
--- /branches/eam_branch_20071023/psModules/templates/h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/templates/h	(revision 15358)
@@ -0,0 +1,13 @@
+/** @file  filename.h
+ *
+ *  @brief Contains the definitions for ...
+ *
+ *  Detailed Description goes here.
+ *
+ *  @author Robert DeSonia, MHPCC
+ *
+ *  @version $Revision: 1.1.1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2004-09-10 19:01:33 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
Index: /branches/eam_branch_20071023/psModules/test/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/.cvsignore	(revision 15358)
@@ -0,0 +1,15 @@
+.deps
+.libs
+Makefile
+tst_pmFlatField
+tst_pmMaskBadPixels
+tst_pmNonLinear
+tst_pmSubtractBias
+temp
+Makefile.in
+tst_pmObjects01
+tst_pmReadoutCombine
+tst_pmSubtractSky
+tst_pmImageSubtract
+tst_pmImageCombine
+tst_pmAstrometry
Index: /branches/eam_branch_20071023/psModules/test/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/Makefile.am	(revision 15358)
@@ -0,0 +1,9 @@
+SUBDIRS = tap pstap $(SRCDIRS)
+
+TESTS = test.pl
+
+EXTRA_DIST = test.pl
+
+CLEANFILES = core core.* *~
+
+test: check
Index: /branches/eam_branch_20071023/psModules/test/astrom/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/astrom/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/astrom/.cvsignore	(revision 15358)
@@ -0,0 +1,12 @@
+.deps
+.libs
+Makefile
+Makefile.in
+temp
+tst_pmAstrometry
+tst_pmAstrometry01
+tap_pmAstrometryWCS
+tap_pmAstrometryWCS_DVO
+tap_pmAstrometryWCS_DVO2
+tap_pmAstrometryWCS_DVO3
+tap_pmAstrometryWCS_DVO4
Index: /branches/eam_branch_20071023/psModules/test/astrom/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/astrom/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/astrom/Makefile.am	(revision 15358)
@@ -0,0 +1,30 @@
+AM_CPPFLAGS = \
+	$(SRCINC) \
+	-I$(top_srcdir)/test/tap/src \
+	-I$(top_srcdir)/test/pstap/src \
+	$(PSMODULES_CFLAGS)
+
+AM_LDFLAGS = \
+	$(top_builddir)/src/libpsmodules.la  \
+	$(top_builddir)/test/tap/src/libtap.la \
+	$(top_builddir)/test/pstap/src/libpstap.la \
+	$(PSMODULES_LIBS)
+
+TEST_PROGS = \
+	tap_pmAstrometryWCS \
+	tap_pmAstrometryWCS_DVO \
+	tap_pmAstrometryWCS_DVO2 \
+	tap_pmAstrometryWCS_DVO3 \
+	tap_pmAstrometryWCS_DVO4
+
+if BUILD_TESTS
+bin_PROGRAMS = $(TEST_PROGS)
+TESTS = $(TEST_PROGS)
+else
+check_PROGRAMS = $(TEST_PROGS)
+endif
+
+CLEANFILES = $(check_DATA) temp/* core core.* *~ *.bb *.bbg *.da gmon.out
+
+test: check
+	$(top_srcdir)/test/test.pl
Index: /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometry.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometry.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometry.c	(revision 15358)
@@ -0,0 +1,318 @@
+    /** @file  tst_pmAstrometry.c
+     *
+ *  @brief Contains the tests: pmAstrometry.[ch].  The pmxxxAlloc()
+ *  and psFree() functionality are used here.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  XXX: Untested: pmFPACheckParents()
+ *  XXX: Create the pmHDU alloc/free function, test them here
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-06-19 18:28:38 $
+ *
+ *  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+#define CHIP_ALLOC_NAME "ChipName"
+#define CELL_ALLOC_NAME "CellName"
+#define MISC_NUM 32
+#define MISC_NAME "META00"
+#define MISC_NAME2 "META01"
+#define NUM_BIAS_DATA 10
+#define TEST_NUM_ROWS 32
+#define TEST_NUM_COLS 32
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    fpa->hdu = pmHDUAlloc(NULL);
+    return(fpa);
+}
+
+
+/******************************************************************************
+testFPAAlloc()
+    1: We ensure that pmFPAAlloc() properly allocates a pmFPA struct.
+    2: We populate the members with real data to ensure they are being
+       free'ed correctly.
+ *****************************************************************************/
+void testFPAAlloc(void)
+{
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = pmFPAAlloc(camera);
+        ok(fpa != NULL, "pmFPAAlloc() returned a non-NULL");
+        ok(fpa->fromTPA == NULL, "pmFPAAlloc() set ->fromTPA to NULL");
+        ok(fpa->toTPA == NULL, "pmFPAAlloc() set ->toTPA to NULL");
+        ok(fpa->toSky == NULL, "pmFPAAlloc() set ->toSky to NULL");
+        ok(fpa->concepts != NULL &&
+           psMemCheckMetadata(fpa->concepts), "pmFPAAlloc() set ->concepts correctly");
+        ok(fpa->conceptsRead == PM_CONCEPT_SOURCE_NONE, "pmFPAAlloc() set ->conceptsRead correctly");
+        ok(fpa->analysis != NULL &&
+           psMemCheckMetadata(fpa->analysis), "pmFPAAlloc() set ->analysis correctly");
+        ok(fpa->camera == camera, "pmFPAAlloc() set ->camera correctly");
+        ok(fpa->chips != NULL &&
+           psMemCheckArray(fpa->chips), "pmFPAAlloc() set ->chips correctly");
+        ok(fpa->hdu == NULL, "pmFPAAlloc() set ->hdu to NULL");
+        ok(fpa->wrote_phu == false, "pmFPAAlloc() set ->wrote_phu to FALSE");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Populate the pmFPA struct with real data to ensure they were
+    // psFree()'ed correctly.
+    {
+        psMemId id = psMemGetId();
+        pmFPA* fpa = generateSimpleFPA(NULL);
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    chip->hdu = pmHDUAlloc(NULL);
+    return(chip);
+}
+
+void testChipAlloc(void)
+{
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+        ok(chip != NULL, "pmChipAlloc() returned non-NULL");
+        ok(chip->toFPA == NULL, "pmChipAlloc() set chip->toChip to NULL");
+        ok(chip->fromFPA == NULL, "pmChipAlloc() set chip->fromChip to NULL");
+        ok(chip->concepts != NULL &&
+           psMemCheckMetadata(chip->concepts), "pmChipAlloc() set ->concepts correctly");
+        ok(chip->conceptsRead == PM_CONCEPT_SOURCE_NONE, "pmCellAlloc() set ->conceptsRead correctly");
+        ok(chip->analysis != NULL &&
+           psMemCheckMetadata(chip->analysis), "pmChipAlloc() set ->analysis correctly");
+        ok(chip->cells != NULL &&
+           psMemCheckArray(chip->cells), "pmChipAlloc() set ->cells correctly");
+        ok(chip->parent == fpa, "pmChipAlloc() set ->parent correctly");
+        ok(chip->process == true, "pmChipAlloc() set ->process correctly");
+        ok(chip->file_exists == false, "pmChipAlloc() set ->file_exists correctly");
+        ok(chip->data_exists == false, "pmChipAlloc() set ->data_exists correctly");
+        ok(chip->hdu == NULL, "pmChipAlloc() set ->hdu to NULL");
+        ok(chip->wrote_phu == false, "pmChipAlloc() set ->wrote_phu correctly");
+        psFree(camera);
+        psFree(fpa);
+        psFree(chip);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Populate the pmChip struct with real data to ensure they were
+    // psFree()'ed correctly.
+    {
+        psMemId id = psMemGetId();
+        pmChip *chip = generateSimpleChip(NULL);
+        psFree(chip);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmFPA *fpa, pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    cell->hdu = pmHDUAlloc(NULL);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(cell);
+}
+
+void testCellAlloc(void)
+{
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+        pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+        ok(cell != NULL, "pmCellAlloc returned non-NULL");
+        ok(cell->concepts != NULL &&
+           psMemCheckMetadata(cell->concepts), "pmCellpAlloc() set ->concepts correctly");
+        ok(cell->conceptsRead == PM_CONCEPT_SOURCE_NONE, "pmCellAlloc() set ->conceptsRead correctly");
+        ok(cell->config == NULL, "pmCellpAlloc() set ->config to NULL");
+        ok(cell->analysis != NULL &&
+           psMemCheckMetadata(cell->analysis), "pmCellAlloc() set ->analysis correctly");
+        ok(cell->readouts != NULL &&
+           psMemCheckArray(cell->readouts), "pmCellAlloc() set ->readouts correctly");
+        ok(cell->parent == chip, "pmCellAlloc() set ->parent correctly");
+        ok(cell->process == true, "pmCellAlloc() set ->process correctly");
+        ok(cell->file_exists == false, "pmCellAlloc() set ->file_exists correctly");
+        ok(cell->data_exists == false, "pmCellAlloc() set ->data_exists correctly");
+        ok(cell->hdu == NULL, "pmCellAlloc() set ->hdu to NULL");
+        ok(cell->wrote_phu == false, "pmCellAlloc() set ->wrote_phu correctly");
+        psFree(camera);
+        psFree(fpa);
+        psFree(chip);
+        psFree(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Populate the pmCell struct with real data to ensure they were
+    // psFree()'ed correctly.
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL, NULL);
+        psFree(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test pmCellFreeReadouts()
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL, NULL);
+        pmCellFreeReadouts(cell);
+        ok(cell->readouts->n == 0, "pmCellFreeReadouts() correctly set cell->readouts->n to 0");
+        psFree(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks (pmCellFreeReadouts)");
+    }
+
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+
+void testReadoutAlloc(void)
+{
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+        pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+        pmReadout *readout = pmReadoutAlloc(cell);
+        ok(readout != NULL, "pmReadoutAlloc() returned non-NULL");
+        ok(readout->col0 == 0, "pmReadoutAlloc() set ->col0 correctly");
+        ok(readout->row0 == 0, "pmReadoutAlloc() set ->row0 correctly");
+        ok(readout->image == NULL, "pmReadoutAlloc() set ->image correctly");
+        ok(readout->mask == NULL, "pmReadoutAlloc() set ->mask correctly");
+        ok(readout->weight == NULL, "pmReadoutAlloc() set ->weight correctly");
+        ok(readout->bias != NULL &&
+           psMemCheckList(readout->bias), "pmReadoutAlloc() set ->bias correctly");
+        ok(readout->analysis != NULL &&
+           psMemCheckMetadata(readout->analysis), "pmReadoutAlloc() set ->analysis correctly");
+        ok(readout->parent == cell, "pmReadoutAlloc() set ->parent correctly");
+        ok(readout->process == true, "pmReadoutAlloc() set ->process correctly");
+        ok(readout->file_exists == false, "pmReadoutAlloc() set ->file_exists correctly");
+        ok(readout->data_exists == false, "pmReadoutAlloc() set ->data_exists correctly");
+        psFree(camera);
+        psFree(fpa);
+        psFree(chip);
+        psFree(cell);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Populate the pmReadout struct with real data to ensure they were
+    // psFree()'ed correctly.
+    {
+        psMemId id = psMemGetId();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        psFree(readout);
+        // XXX: The pmReadout->bias list is not being free'ed correctly.
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+/*
+    {
+        psMemId id = psMemGetId();
+        pmReadout *readout = pmReadoutAlloc(NULL);
+        readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+        readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+            psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+            psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        }
+        psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+        return(readout);
+    }
+*/
+}
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    plan_tests(56);
+
+    testFPAAlloc();
+    testChipAlloc();
+    testCellAlloc();
+    testReadoutAlloc();
+}
Index: /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS.c	(revision 15358)
@@ -0,0 +1,151 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+int main (void)
+{
+    plan_tests(33);
+
+    note("pmAstrometryWCS tests");
+
+    {
+        note("test pmAstromReadWCS");
+        psMemId id = psMemGetId();
+
+        // construct a header with a simple set of WCS values
+        // convert to pmFPA components and check
+
+        psMetadata *header = psMetadataAlloc();
+
+        psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE1", PS_META_REPLACE, "", "RA---TAN");
+        psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE2", PS_META_REPLACE, "", "DEC--TAN");
+
+        // center coords (R,D)
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CRVAL1", PS_META_REPLACE, "", 0.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CRVAL2", PS_META_REPLACE, "", 0.0);
+
+        // center coords (X,Y)
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CRPIX1", PS_META_REPLACE, "", 0.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CRPIX2", PS_META_REPLACE, "", 0.0);
+
+        // degrees per pixel
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CDELT1",  PS_META_REPLACE, "", 1.0/3600.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CDELT2",  PS_META_REPLACE, "", 1.0/3600.0);
+
+        // rotation matrix
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PC001001", PS_META_REPLACE, "", 1.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PC001002", PS_META_REPLACE, "", 0.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PC002001", PS_META_REPLACE, "", 0.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PC002002", PS_META_REPLACE, "", 1.0);
+
+        pmFPA *fpa = pmFPAAlloc (NULL);
+        pmChip *chip = pmChipAlloc (NULL, "test");
+
+        // toFPA carries pixel scale (microns per pixel)
+        // toSky carries plate scale (radians per micron)
+        bool status = pmAstromReadWCS (fpa, chip, header, 10.0);
+        ok (status, "converted WCS keywords to FPA astrometry");
+        skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+        ok(fpa->toSky->type == PS_PROJ_TAN, "correct projection (TAN)");
+
+        // make these tests double
+        ok_float(fpa->toSky->R*PM_DEG_RAD, 0.0, "projection center RA %f", fpa->toSky->R*PM_DEG_RAD);
+        ok_float(fpa->toSky->R*PM_DEG_RAD, 0.0, "projection center DEC %f", fpa->toSky->R*PM_DEG_RAD);
+
+        ok_float(fpa->toSky->Xs, PM_RAD_DEG/3600.0/10.0, "projection X scale %f", fpa->toSky->Xs);
+        ok_float(fpa->toSky->Ys, PM_RAD_DEG/3600.0/10.0, "projection X scale %f", fpa->toSky->Ys);
+
+        ok_float(fpa->toTPA->x->coeff[1][0], 1.0, "TP scale (unity): %f", fpa->toTPA->x->coeff[1][0]);
+        ok_float(fpa->toTPA->y->coeff[0][1], 1.0, "TP scale (unity): %f", fpa->toTPA->x->coeff[1][0]);
+
+        ok_float(chip->toFPA->x->coeff[0][0], 0.0, "ref pixel X: %f", chip->toFPA->x->coeff[0][0]);
+        ok_float(chip->toFPA->y->coeff[0][0], 0.0, "ref pixel Y: %f", chip->toFPA->y->coeff[0][0]);
+
+        ok_float(chip->toFPA->x->coeff[1][0], 10.0, "CD1_1: %f", chip->toFPA->x->coeff[1][0]);
+        ok_float(chip->toFPA->x->coeff[0][1], 0.0, "CD1_2: %f", chip->toFPA->x->coeff[0][1]);
+        ok_float(chip->toFPA->y->coeff[1][0], 0.0, "CD2_1: %f", chip->toFPA->y->coeff[1][0]);
+        ok_float(chip->toFPA->y->coeff[0][1], 10.0, "CD2_2: %f", chip->toFPA->y->coeff[0][1]);
+
+        psFree (fpa);
+        psFree (chip);
+        psFree (header);
+
+        skip_end();
+        ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+    }
+
+    {
+        note("test pmAstromReadWCS");
+        psMemId id = psMemGetId();
+
+        // construct a header with a simple set of WCS values
+        // convert to pmFPA components and check
+
+        psMetadata *header = psMetadataAlloc();
+
+        psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE1", PS_META_REPLACE, "", "RA---TAN");
+        psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE2", PS_META_REPLACE, "", "DEC--TAN");
+
+        // center coords (R,D)
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CRVAL1", PS_META_REPLACE, "", 0.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CRVAL2", PS_META_REPLACE, "", 0.0);
+
+        // center coords (X,Y)
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CRPIX1", PS_META_REPLACE, "", 10.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CRPIX2", PS_META_REPLACE, "", 10.0);
+
+        // degrees per pixel
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CDELT1",  PS_META_REPLACE, "", 1.0/3600.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "CDELT2",  PS_META_REPLACE, "", 1.0/3600.0);
+
+        // rotation matrix
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PC001001", PS_META_REPLACE, "", 1.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PC001002", PS_META_REPLACE, "", 0.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PC002001", PS_META_REPLACE, "", 0.0);
+        psMetadataAddF32 (header, PS_LIST_TAIL, "PC002002", PS_META_REPLACE, "", 1.0);
+
+        pmFPA *fpa = pmFPAAlloc (NULL);
+        pmChip *chip = pmChipAlloc (NULL, "test");
+
+        // toFPA carries pixel scale (pixels per micron)
+        // toTPA carries plate scale (microns per arcsecond)
+        bool status = pmAstromReadWCS (fpa, chip, header, 10.0);
+        ok (status, "converted WCS keywords to FPA astrometry");
+        skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+        ok(fpa->toSky->type == PS_PROJ_TAN, "correct projection (TAN)");
+
+        // make these tests double
+        ok_float(fpa->toSky->R*PM_DEG_RAD, 0.0, "projection center RA %f", fpa->toSky->R*PM_DEG_RAD);
+        ok_float(fpa->toSky->R*PM_DEG_RAD, 0.0, "projection center DEC %f", fpa->toSky->R*PM_DEG_RAD);
+
+        ok_float(fpa->toSky->Xs, PM_RAD_DEG/3600.0/10.0, "projection X scale %f", fpa->toSky->Xs);
+        ok_float(fpa->toSky->Ys, PM_RAD_DEG/3600.0/10.0, "projection X scale %f", fpa->toSky->Ys);
+
+        ok_float(fpa->toTPA->x->coeff[1][0], 1.0, "TP scale (unity): %f", fpa->toTPA->x->coeff[1][0]);
+        ok_float(fpa->toTPA->y->coeff[0][1], 1.0, "TP scale (unity): %f", fpa->toTPA->x->coeff[1][0]);
+
+        ok_float(chip->toFPA->x->coeff[0][0], -100.0, "ref pixel X: %f", chip->toFPA->x->coeff[0][0]);
+        ok_float(chip->toFPA->y->coeff[0][0], -100.0, "ref pixel Y: %f", chip->toFPA->y->coeff[0][0]);
+
+        ok_float(chip->toFPA->x->coeff[1][0], 10.0, "CD1_1: %f", chip->toFPA->x->coeff[1][0]);
+        ok_float(chip->toFPA->x->coeff[0][1], 0.0, "CD1_2: %f", chip->toFPA->x->coeff[0][1]);
+        ok_float(chip->toFPA->y->coeff[1][0], 0.0, "CD2_1: %f", chip->toFPA->y->coeff[1][0]);
+        ok_float(chip->toFPA->y->coeff[0][1], 10.0, "CD2_2: %f", chip->toFPA->y->coeff[0][1]);
+
+        psFree (fpa);
+        psFree (chip);
+        psFree (header);
+
+        skip_end();
+        ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+    }
+
+    return exit_status();
+}
+
Index: /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO.c	(revision 15358)
@@ -0,0 +1,527 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+# if (HAVE_KAPA)
+    # include "dvo.h"
+
+    psMetadata *WriteCoordsToHeader (Coords *coords);
+void test1(); // basic TAN projection,
+void test2(); // small rotation
+void test3(); // 2nd order term
+
+void test1in(); // basic TAN projection,
+void test2in(); // small rotation
+void test3in(); // 2nd order term
+
+void testA(); // alloc test
+void testB(); // alloc test
+
+int main (void)
+{
+    plan_tests(992);
+
+    note("pmAstrometryWCS tests compared with DVO coords routines");
+    note("this file tests pmAstromWCS <-> Header representations");
+
+    test1in();
+    test2in();
+    test3in();
+
+    test1();
+    test2();
+    test3();
+    return exit_status();
+}
+
+void test3in()
+{
+    note("test pmAstromWCStoHeader");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 2;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+    coords.polyterms[0][0] = 0.01; // L vs X^2
+    coords.polyterms[2][1] = 0.01; // M vs Y^2
+
+    psMetadata *header1 = WriteCoordsToHeader(&coords);
+    pmAstromWCS *wcs1 = pmAstromWCSfromHeader(header1);
+    skip_start (wcs1 == NULL, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psMetadata *header2 = psMetadataAlloc();
+    pmAstromWCStoHeader(header2, wcs1);
+    pmAstromWCS *wcs2 = pmAstromWCSfromHeader(header2);
+
+    ok (wcs2 != NULL, "converted WCS keywords to WCS astrometry");
+    skip_start (wcs2 == NULL, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psSphere *sky1 = psSphereAlloc();
+    psSphere *sky2 = psSphereAlloc();
+    psPlane *chip = psPlaneAlloc();
+
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            chip->x = x;
+            chip->y = y;
+            pmAstromWCStoSky (sky1, wcs1, chip);
+            pmAstromWCStoSky (sky2, wcs2, chip);
+            while (sky1->r > 2*M_PI)
+                sky1->r -= 2*M_PI;
+            while (sky1->r <      0)
+                sky1->r += 2*M_PI;
+            while (sky2->r > 2*M_PI)
+                sky2->r -= 2*M_PI;
+            while (sky2->r <      0)
+                sky2->r += 2*M_PI;
+
+            ok_float(sky1->r*PM_DEG_RAD, sky2->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->r*PM_DEG_RAD, sky2->r*PM_DEG_RAD, sky1->r*PM_DEG_RAD - sky2->r*PM_DEG_RAD);
+            ok_float(sky1->d*PM_DEG_RAD, sky2->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->d*PM_DEG_RAD, sky2->d*PM_DEG_RAD, sky1->d*PM_DEG_RAD - sky2->d*PM_DEG_RAD);
+        }
+    }
+    psFree (sky1);
+    psFree (sky2);
+    psFree (chip);
+
+    skip_end();
+    psFree (wcs2);
+    psFree (header2);
+
+    skip_end();
+    psFree (wcs1);
+    psFree (header1);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test2in()
+{
+    note("test pmAstromWCStoHeader");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 0.9;
+    coords.pc1_2  = -0.1;
+    coords.pc2_1  = 0.1;
+    coords.pc2_2  = 0.9;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header1 = WriteCoordsToHeader(&coords);
+    pmAstromWCS *wcs1 = pmAstromWCSfromHeader(header1);
+    skip_start (wcs1 == NULL, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psMetadata *header2 = psMetadataAlloc();
+    pmAstromWCStoHeader(header2, wcs1);
+    pmAstromWCS *wcs2 = pmAstromWCSfromHeader(header2);
+
+    ok (wcs2 != NULL, "converted WCS keywords to WCS astrometry");
+    skip_start (wcs2 == NULL, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psSphere *sky1 = psSphereAlloc();
+    psSphere *sky2 = psSphereAlloc();
+    psPlane *chip = psPlaneAlloc();
+
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            chip->x = x;
+            chip->y = y;
+            pmAstromWCStoSky (sky1, wcs1, chip);
+            pmAstromWCStoSky (sky2, wcs2, chip);
+            while (sky1->r > 2*M_PI)
+                sky1->r -= 2*M_PI;
+            while (sky1->r <      0)
+                sky1->r += 2*M_PI;
+            while (sky2->r > 2*M_PI)
+                sky2->r -= 2*M_PI;
+            while (sky2->r <      0)
+                sky2->r += 2*M_PI;
+
+            ok_float(sky1->r*PM_DEG_RAD, sky2->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->r*PM_DEG_RAD, sky2->r*PM_DEG_RAD, sky1->r*PM_DEG_RAD - sky2->r*PM_DEG_RAD);
+            ok_float(sky1->d*PM_DEG_RAD, sky2->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->d*PM_DEG_RAD, sky2->d*PM_DEG_RAD, sky1->d*PM_DEG_RAD - sky2->d*PM_DEG_RAD);
+        }
+    }
+    psFree (sky1);
+    psFree (sky2);
+    psFree (chip);
+
+    skip_end();
+    psFree (wcs2);
+    psFree (header2);
+
+    skip_end();
+    psFree (wcs1);
+    psFree (header1);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test1in()
+{
+    note("test pmAstromWCStoHeader");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header1 = WriteCoordsToHeader(&coords);
+    pmAstromWCS *wcs1 = pmAstromWCSfromHeader(header1);
+    skip_start (wcs1 == NULL, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psMetadata *header2 = psMetadataAlloc();
+    pmAstromWCStoHeader(header2, wcs1);
+    pmAstromWCS *wcs2 = pmAstromWCSfromHeader(header2);
+
+    ok (wcs2 != NULL, "converted WCS keywords to WCS astrometry");
+    skip_start (wcs2 == NULL, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psSphere *sky1 = psSphereAlloc();
+    psSphere *sky2 = psSphereAlloc();
+    psPlane *chip = psPlaneAlloc();
+
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            chip->x = x;
+            chip->y = y;
+            pmAstromWCStoSky (sky1, wcs1, chip);
+            pmAstromWCStoSky (sky2, wcs2, chip);
+            while (sky1->r > 2*M_PI)
+                sky1->r -= 2*M_PI;
+            while (sky1->r <      0)
+                sky1->r += 2*M_PI;
+            while (sky2->r > 2*M_PI)
+                sky2->r -= 2*M_PI;
+            while (sky2->r <      0)
+                sky2->r += 2*M_PI;
+
+            ok_float(sky1->r*PM_DEG_RAD, sky2->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->r*PM_DEG_RAD, sky2->r*PM_DEG_RAD, sky1->r*PM_DEG_RAD - sky2->r*PM_DEG_RAD);
+            ok_float(sky1->d*PM_DEG_RAD, sky2->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", sky1->d*PM_DEG_RAD, sky2->d*PM_DEG_RAD, sky2->d*PM_DEG_RAD - sky2->d*PM_DEG_RAD);
+        }
+    }
+    psFree (sky1);
+    psFree (sky2);
+    psFree (chip);
+
+    skip_end();
+    psFree (wcs2);
+    psFree (header2);
+
+    skip_end();
+    psFree (wcs1);
+    psFree (header1);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test3 ()
+{
+    note("test pmAstromWCSfromHeader ");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 2;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+    coords.polyterms[0][0] = 0.01; // L vs X^2
+    coords.polyterms[2][1] = 0.01; // M vs Y^2
+
+    psMetadata *header = WriteCoordsToHeader (&coords);
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+
+    ok (wcs != NULL, "converted WCS keywords to WCS astrometry");
+    skip_start (wcs == NULL, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psSphere *sky = psSphereAlloc();
+    psPlane *chip = psPlaneAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+            chip->x = x;
+            chip->y = y;
+            pmAstromWCStoSky (sky, wcs, chip);
+            while (sky->r > 2*M_PI)
+                sky->r -= 2*M_PI;
+            while (sky->r <      0)
+                sky->r += 2*M_PI;
+
+            ok_float(rDVO, sky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, sky->r*PM_DEG_RAD, rDVO - sky->r*PM_DEG_RAD);
+            ok_float(dDVO, sky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, sky->d*PM_DEG_RAD, dDVO - sky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (sky);
+    psFree (chip);
+
+    skip_end();
+    psFree (wcs);
+    psFree (header);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test2 ()
+{
+    note("test pmAstromWCSfromHeader");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 0.9;
+    coords.pc1_2  = -0.1;
+    coords.pc2_1  = 0.1;
+    coords.pc2_2  = 0.9;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header = WriteCoordsToHeader (&coords);
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+
+    ok (wcs != NULL, "converted WCS keywords to WCS astrometry");
+    skip_start (wcs == NULL, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psSphere *sky = psSphereAlloc();
+    psPlane *chip = psPlaneAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+            chip->x = x;
+            chip->y = y;
+            pmAstromWCStoSky (sky, wcs, chip);
+            while (sky->r > 2*M_PI)
+                sky->r -= 2*M_PI;
+            while (sky->r <      0)
+                sky->r += 2*M_PI;
+
+            ok_float(rDVO, sky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, sky->r*PM_DEG_RAD, rDVO - sky->r*PM_DEG_RAD);
+            ok_float(dDVO, sky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, sky->d*PM_DEG_RAD, dDVO - sky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (sky);
+    psFree (chip);
+
+    skip_end();
+    psFree (wcs);
+    psFree (header);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test1()
+{
+    note("test pmAstromWCSfromHeader");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header = WriteCoordsToHeader (&coords);
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+
+    ok (wcs != NULL, "converted WCS keywords to WCS astrometry");
+    skip_start (wcs == NULL, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psSphere *sky = psSphereAlloc();
+    psPlane *chip = psPlaneAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+            chip->x = x;
+            chip->y = y;
+            pmAstromWCStoSky (sky, wcs, chip);
+            while (sky->r > 2*M_PI)
+                sky->r -= 2*M_PI;
+            while (sky->r <      0)
+                sky->r += 2*M_PI;
+
+            ok_float(rDVO, sky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, sky->r*PM_DEG_RAD, rDVO - sky->r*PM_DEG_RAD);
+            ok_float(dDVO, sky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, sky->d*PM_DEG_RAD, dDVO - sky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (sky);
+    psFree (chip);
+
+    skip_end();
+    psFree (wcs);
+    psFree (header);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void testA()
+{
+    note("test coord allocs");
+    psMemId id = psMemGetId();
+
+    psPlane *chip = psPlaneAlloc();
+    psFree (chip);
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void testB()
+{
+    note("test coord allocs");
+    psMemId id = psMemGetId();
+
+    psSphere *sky = psSphereAlloc();
+    psFree (sky);
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+psMetadata *WriteCoordsToHeader (Coords *coords)
+{
+
+    char name[16];
+
+    // construct a header using coords as the input
+    psMetadata *header = psMetadataAlloc();
+
+    sprintf (name, "RA--%s", &coords[0].ctype[4]);
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE1", PS_META_REPLACE, "", name);
+    sprintf (name, "DEC-%s", &coords[0].ctype[4]);
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE2", PS_META_REPLACE, "", name);
+
+    // center coords (R,D)
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRVAL1", PS_META_REPLACE, "", coords[0].crval1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRVAL2", PS_META_REPLACE, "", coords[0].crval2);
+
+    // center coords (X,Y)
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRPIX1", PS_META_REPLACE, "", coords[0].crpix1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRPIX2", PS_META_REPLACE, "", coords[0].crpix2);
+
+    // degrees per pixel
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CDELT1",  PS_META_REPLACE, "", coords[0].cdelt1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CDELT2",  PS_META_REPLACE, "", coords[0].cdelt2);
+
+    // rotation matrix
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC001001", PS_META_REPLACE, "", coords[0].pc1_1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC001002", PS_META_REPLACE, "", coords[0].pc1_2);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC002001", PS_META_REPLACE, "", coords[0].pc2_1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC002002", PS_META_REPLACE, "", coords[0].pc2_2);
+
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X2Y0", PS_META_REPLACE, "", coords[0].polyterms[0][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X1Y1", PS_META_REPLACE, "", coords[0].polyterms[1][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X0Y2", PS_META_REPLACE, "", coords[0].polyterms[2][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X2Y0", PS_META_REPLACE, "", coords[0].polyterms[0][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X1Y1", PS_META_REPLACE, "", coords[0].polyterms[1][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X0Y2", PS_META_REPLACE, "", coords[0].polyterms[2][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X3Y0", PS_META_REPLACE, "", coords[0].polyterms[3][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X2Y1", PS_META_REPLACE, "", coords[0].polyterms[4][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X1Y2", PS_META_REPLACE, "", coords[0].polyterms[5][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X0Y3", PS_META_REPLACE, "", coords[0].polyterms[6][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X3Y0", PS_META_REPLACE, "", coords[0].polyterms[3][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X2Y1", PS_META_REPLACE, "", coords[0].polyterms[4][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X1Y2", PS_META_REPLACE, "", coords[0].polyterms[5][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X0Y3", PS_META_REPLACE, "", coords[0].polyterms[6][1]);
+
+    psMetadataAddS32 (header, PS_LIST_TAIL, "NPLYTERM", PS_META_REPLACE, "", coords[0].Npolyterms);
+
+    return header;
+}
+
+# else
+
+    int main (void)
+{
+    plan_tests(0);
+
+    note("pmAstrometryWCS tests compared with DVO coords routines : SKIPPED (libdvo not available)");
+
+    return exit_status();
+}
+
+# endif
+
Index: /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO2.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO2.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO2.c	(revision 15358)
@@ -0,0 +1,629 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+# if (HAVE_KAPA)
+    # include "dvo.h"
+
+    psMetadata *WriteCoordsToHeader (Coords *coords);
+void test1(); // basic TAN projection,
+void test2(); // small rotation
+void test3(); // 2nd order term
+void test1x(); // basic TAN projection with central offset
+void test2x(); // small rotation with central offset
+void test3x(); // 2nd order term with central offset
+void test3inv(); // 2nd order term with central offset
+
+int main (void)
+{
+    plan_tests(1483);
+
+    note("pmAstromReadWCS tests compared with DVO coords routines");
+
+    test1();
+    test2();
+    test3();
+    test1x();
+    test2x();
+    test3x();
+    test3inv();
+
+    return exit_status();
+}
+
+void test1()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header = WriteCoordsToHeader (&coords);
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header, PM_RAD_DEG*10.0/3600.0);
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane *onChip = psPlaneAlloc();
+    psPlane *onFPA = psPlaneAlloc();
+    psPlane *onTPA = psPlaneAlloc();
+    psSphere *onSky = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            ok_float(rDVO, onSky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PM_DEG_RAD, rDVO - onSky->r*PM_DEG_RAD);
+            ok_float(dDVO, onSky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PM_DEG_RAD, dDVO - onSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test2()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 0.9;
+    coords.pc1_2  = 0.1;
+    coords.pc2_1  =-0.1;
+    coords.pc2_2  = 0.9;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header = WriteCoordsToHeader (&coords);
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header, PM_RAD_DEG*10.0/3600.0);
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane *onChip = psPlaneAlloc();
+    psPlane *onFPA = psPlaneAlloc();
+    psPlane *onTPA = psPlaneAlloc();
+    psSphere *onSky = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            ok_float(rDVO, onSky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PM_DEG_RAD, rDVO - onSky->r*PM_DEG_RAD);
+            ok_float(dDVO, onSky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PM_DEG_RAD, dDVO - onSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test3()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 2;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+    coords.polyterms[0][0] = 0.01; // L vs X^2
+    coords.polyterms[2][1] = 0.01; // M vs Y^2
+
+
+    psMetadata *header = WriteCoordsToHeader (&coords);
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header, PM_RAD_DEG*10.0/3600.0);
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane *onChip = psPlaneAlloc();
+    psPlane *onFPA = psPlaneAlloc();
+    psPlane *onTPA = psPlaneAlloc();
+    psSphere *onSky = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            ok_float(rDVO, onSky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PM_DEG_RAD, rDVO - onSky->r*PM_DEG_RAD);
+            ok_float(dDVO, onSky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PM_DEG_RAD, dDVO - onSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test1x()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = +50.0;
+    coords.crpix2 = -20.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header = WriteCoordsToHeader (&coords);
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header, PM_RAD_DEG*10.0/3600.0);
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane *onChip = psPlaneAlloc();
+    psPlane *onFPA = psPlaneAlloc();
+    psPlane *onTPA = psPlaneAlloc();
+    psSphere *onSky = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            ok_float(rDVO, onSky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PM_DEG_RAD, rDVO - onSky->r*PM_DEG_RAD);
+            ok_float(dDVO, onSky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PM_DEG_RAD, dDVO - onSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test2x()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = +50.0;
+    coords.crpix2 = -20.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 0.9;
+    coords.pc1_2  = 0.1;
+    coords.pc2_1  =-0.1;
+    coords.pc2_2  = 0.9;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header = WriteCoordsToHeader (&coords);
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header, PM_RAD_DEG*10.0/3600.0);
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane *onChip = psPlaneAlloc();
+    psPlane *onFPA = psPlaneAlloc();
+    psPlane *onTPA = psPlaneAlloc();
+    psSphere *onSky = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            ok_float(rDVO, onSky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PM_DEG_RAD, rDVO - onSky->r*PM_DEG_RAD);
+            ok_float(dDVO, onSky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PM_DEG_RAD, dDVO - onSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test3x()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = +50.0;
+    coords.crpix2 = -20.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 2;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+    coords.polyterms[0][0] = 0.01; // L vs X^2
+    coords.polyterms[2][1] = 0.01; // M vs Y^2
+
+
+    psMetadata *header = WriteCoordsToHeader (&coords);
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header, PM_RAD_DEG*10.0/3600.0);
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane *onChip = psPlaneAlloc();
+    psPlane *onFPA = psPlaneAlloc();
+    psPlane *onTPA = psPlaneAlloc();
+    psSphere *onSky = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            ok_float(rDVO, onSky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", rDVO, onSky->r*PM_DEG_RAD, rDVO - onSky->r*PM_DEG_RAD);
+            ok_float(dDVO, onSky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", dDVO, onSky->d*PM_DEG_RAD, dDVO - onSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test3inv()
+{
+    note("test the inversion of the non-linear polynomial for toFPA -> fromFPA in pmAstromReadWCS");
+    note("note that the tolerance for these tests are rather loose");
+    note("a 2nd order polynomial is not a great approximate to 1 over a 2nd order polynomial");
+    note("unless the non-linear terms are quite small");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = +50.0;
+    coords.crpix2 = -20.0;
+    coords.cdelt1 = 1.0/3600;
+    coords.cdelt2 = 1.0/3600;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 2;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+    coords.polyterms[0][0] = 0.01; // L vs X^2
+    coords.polyterms[2][1] = 0.01; // M vs Y^2
+
+
+    psMetadata *header = WriteCoordsToHeader (&coords);
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header, PM_RAD_DEG*10.0/3600.0);
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane *aChip  = psPlaneAlloc();
+    psPlane *aFPA   = psPlaneAlloc();
+    psPlane *aTPA   = psPlaneAlloc();
+    psPlane *bChip  = psPlaneAlloc();
+    psPlane *bFPA   = psPlaneAlloc();
+    psPlane *bTPA   = psPlaneAlloc();
+    psSphere *onSky = psSphereAlloc();
+
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            // convert up to sky
+            aChip->x = x;
+            aChip->y = y;
+
+            psPlaneTransformApply (aFPA, chip->toFPA, aChip);
+            psPlaneTransformApply (aTPA, fpa->toTPA, aFPA);
+            psDeproject (onSky, aTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            psProject (bTPA, onSky, fpa->toSky);
+            psPlaneTransformApply (bFPA, fpa->fromTPA, bTPA);
+            psPlaneTransformApply (bChip, chip->fromFPA, bFPA);
+
+            // calculate appropriate tol values as f(x,y)
+            ok_float_tol(aChip->x, bChip->x, 1.0, "coordinate match: %f vs %f (delta = %f)", aChip->x, bChip->x, aChip->x - bChip->x);
+            ok_float_tol(aChip->y, bChip->y, 1.0, "coordinate match: %f vs %f (delta = %f)", aChip->y, bChip->y, aChip->y - bChip->y);
+
+            ok_float(aFPA->x, bFPA->x, "coordinate match: %f vs %f (delta = %f)", aFPA->x, bFPA->x, aFPA->x - bFPA->x);
+            ok_float(aFPA->y, bFPA->y, "coordinate match: %f vs %f (delta = %f)", aFPA->y, bFPA->y, aFPA->y - bFPA->y);
+
+            // in this example, TPA coordinates are 10 arcsec/mm; the tol. below represent 1nano-arcsec
+            ok_float_tol(aTPA->x, bTPA->x, 1e-10, "coordinate match: %f vs %f (delta = %g)", aTPA->x, bTPA->x, aTPA->x - bTPA->x);
+            ok_float_tol(aTPA->y, bTPA->y, 1e-10, "coordinate match: %f vs %f (delta = %g)", aTPA->y, bTPA->y, aTPA->y - bTPA->y);
+        }
+    }
+    psFree (onSky);
+    psFree (aTPA);
+    psFree (aFPA);
+    psFree (aChip);
+    psFree (bTPA);
+    psFree (bFPA);
+    psFree (bChip);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+psMetadata *WriteCoordsToHeader (Coords *coords)
+{
+
+    char name[16];
+
+    // construct a header using coords as the input
+    psMetadata *header = psMetadataAlloc();
+
+    sprintf (name, "RA--%s", &coords[0].ctype[4]);
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE1", PS_META_REPLACE, "", name);
+    sprintf (name, "DEC-%s", &coords[0].ctype[4]);
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE2", PS_META_REPLACE, "", name);
+
+    // center coords (R,D)
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRVAL1", PS_META_REPLACE, "", coords[0].crval1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRVAL2", PS_META_REPLACE, "", coords[0].crval2);
+
+    // center coords (X,Y)
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRPIX1", PS_META_REPLACE, "", coords[0].crpix1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRPIX2", PS_META_REPLACE, "", coords[0].crpix2);
+
+    // degrees per pixel
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CDELT1",  PS_META_REPLACE, "", coords[0].cdelt1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CDELT2",  PS_META_REPLACE, "", coords[0].cdelt2);
+
+    // rotation matrix
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC001001", PS_META_REPLACE, "", coords[0].pc1_1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC001002", PS_META_REPLACE, "", coords[0].pc1_2);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC002001", PS_META_REPLACE, "", coords[0].pc2_1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC002002", PS_META_REPLACE, "", coords[0].pc2_2);
+
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X2Y0", PS_META_REPLACE, "", coords[0].polyterms[0][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X1Y1", PS_META_REPLACE, "", coords[0].polyterms[1][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X0Y2", PS_META_REPLACE, "", coords[0].polyterms[2][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X2Y0", PS_META_REPLACE, "", coords[0].polyterms[0][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X1Y1", PS_META_REPLACE, "", coords[0].polyterms[1][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X0Y2", PS_META_REPLACE, "", coords[0].polyterms[2][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X3Y0", PS_META_REPLACE, "", coords[0].polyterms[3][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X2Y1", PS_META_REPLACE, "", coords[0].polyterms[4][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X1Y2", PS_META_REPLACE, "", coords[0].polyterms[5][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X0Y3", PS_META_REPLACE, "", coords[0].polyterms[6][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X3Y0", PS_META_REPLACE, "", coords[0].polyterms[3][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X2Y1", PS_META_REPLACE, "", coords[0].polyterms[4][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X1Y2", PS_META_REPLACE, "", coords[0].polyterms[5][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X0Y3", PS_META_REPLACE, "", coords[0].polyterms[6][1]);
+
+    psMetadataAddS32 (header, PS_LIST_TAIL, "NPLYTERM", PS_META_REPLACE, "", coords[0].Npolyterms);
+
+    return header;
+}
+
+# else
+
+    int main (void)
+{
+    plan_tests(0);
+
+    note("pmAstrometryWCS tests compared with DVO coords routines : SKIPPED (libdvo not available)");
+
+    return exit_status();
+}
+
+# endif
+
Index: /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO3.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO3.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO3.c	(revision 15358)
@@ -0,0 +1,654 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+# if (HAVE_KAPA)
+    # include "dvo.h"
+
+    psMetadata *WriteCoordsToHeader (Coords *coords);
+void test1(); // basic TAN projection,
+void test2(); // small rotation
+void test3(); // 2nd order term
+void test1x(); // basic TAN projection with central offset
+void test2x(); // small rotation with central offset
+void test3x(); // 2nd order term with central offset
+
+int main (void)
+{
+    plan_tests(991);
+
+    note("pmAstromWriteWCS tests compared with DVO coords routines");
+
+    test1();
+    test2();
+    test3();
+    test1x();
+    test2x();
+    test3x();
+
+    return exit_status();
+}
+
+void test1()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600.0;
+    coords.cdelt2 = 1.0/3600.0;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header1 = WriteCoordsToHeader (&coords);
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header1, 10.0);
+
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psMetadata *header2 = psMetadataAlloc();
+    status = pmAstromWriteWCS (header2, fpa, chip, 0.001);
+    pmAstromWCS *wcs = pmAstromWCSfromHeader(header2);
+
+    psPlane  *aChip = psPlaneAlloc();
+    psPlane  *aFPA = psPlaneAlloc();
+    psPlane  *aTPA = psPlaneAlloc();
+    psSphere *aSky = psSphereAlloc();
+
+    psPlane  *bChip = psPlaneAlloc();
+    psSphere *bSky = psSphereAlloc();
+
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            aChip->x = x;
+            aChip->y = y;
+            bChip->x = x;
+            bChip->y = y;
+
+            psPlaneTransformApply (aFPA, chip->toFPA, aChip);
+            psPlaneTransformApply (aTPA, fpa->toTPA, aFPA);
+            psDeproject (aSky, aTPA, fpa->toSky);
+
+            pmAstromWCStoSky (bSky, wcs, bChip);
+
+            while (aSky->r > 2*M_PI)
+                aSky->r -= 2*M_PI;
+            while (aSky->r <      0)
+                aSky->r += 2*M_PI;
+            while (bSky->r > 2*M_PI)
+                bSky->r -= 2*M_PI;
+            while (bSky->r <      0)
+                bSky->r += 2*M_PI;
+
+            ok_float(aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, aSky->r*PM_DEG_RAD - bSky->r*PM_DEG_RAD);
+            ok_float(aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, aSky->d*PM_DEG_RAD - bSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (aSky);
+    psFree (aTPA);
+    psFree (aFPA);
+    psFree (aChip);
+
+    psFree (bSky);
+    psFree (bChip);
+
+    psFree (wcs);
+    psFree (header2);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header1);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test2()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600.0;
+    coords.cdelt2 = 1.0/3600.0;
+    coords.pc1_1  = 0.9;
+    coords.pc1_2  = 0.1;
+    coords.pc2_1  = -0.1;
+    coords.pc2_2  = 0.9;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header1 = WriteCoordsToHeader (&coords);
+
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header1, 10.0);
+
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psMetadata *header2 = psMetadataAlloc();
+    status = pmAstromWriteWCS (header2, fpa, chip, 0.001);
+
+    pmAstromWCS *wcs = pmAstromWCSfromHeader(header2);
+
+    psPlane  *aChip = psPlaneAlloc();
+    psPlane  *aFPA = psPlaneAlloc();
+    psPlane  *aTPA = psPlaneAlloc();
+    psSphere *aSky = psSphereAlloc();
+
+    psPlane  *bChip = psPlaneAlloc();
+    psSphere *bSky = psSphereAlloc();
+
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            aChip->x = x;
+            aChip->y = y;
+            bChip->x = x;
+            bChip->y = y;
+
+            psPlaneTransformApply (aFPA, chip->toFPA, aChip);
+            psPlaneTransformApply (aTPA, fpa->toTPA, aFPA);
+            psDeproject (aSky, aTPA, fpa->toSky);
+
+            pmAstromWCStoSky (bSky, wcs, bChip);
+
+            while (aSky->r > 2*M_PI)
+                aSky->r -= 2*M_PI;
+            while (aSky->r <      0)
+                aSky->r += 2*M_PI;
+            while (bSky->r > 2*M_PI)
+                bSky->r -= 2*M_PI;
+            while (bSky->r <      0)
+                bSky->r += 2*M_PI;
+
+            ok_float(aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, aSky->r*PM_DEG_RAD - bSky->r*PM_DEG_RAD);
+            ok_float(aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, aSky->d*PM_DEG_RAD - bSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (aSky);
+    psFree (aTPA);
+    psFree (aFPA);
+    psFree (aChip);
+
+    psFree (bSky);
+    psFree (bChip);
+
+    psFree (wcs);
+    psFree (header2);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header1);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test3()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 1.0/3600.0;
+    coords.cdelt2 = 1.0/3600.0;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 2;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+    coords.polyterms[0][0] = 0.01; // L vs X^2
+    coords.polyterms[2][1] = 0.01; // M vs Y^2
+
+    psMetadata *header1 = WriteCoordsToHeader (&coords);
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header1, 10.0);
+
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psMetadata *header2 = psMetadataAlloc();
+    status = pmAstromWriteWCS (header2, fpa, chip, 0.001);
+
+    pmAstromWCS *wcs = pmAstromWCSfromHeader(header2);
+
+    psPlane  *aChip = psPlaneAlloc();
+    psPlane  *aFPA = psPlaneAlloc();
+    psPlane  *aTPA = psPlaneAlloc();
+    psSphere *aSky = psSphereAlloc();
+
+    psPlane  *bChip = psPlaneAlloc();
+    psSphere *bSky = psSphereAlloc();
+
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            aChip->x = x;
+            aChip->y = y;
+            bChip->x = x;
+            bChip->y = y;
+
+            psPlaneTransformApply (aFPA, chip->toFPA, aChip);
+            psPlaneTransformApply (aTPA, fpa->toTPA, aFPA);
+            psDeproject (aSky, aTPA, fpa->toSky);
+
+            pmAstromWCStoSky (bSky, wcs, bChip);
+
+            while (aSky->r > 2*M_PI)
+                aSky->r -= 2*M_PI;
+            while (aSky->r <      0)
+                aSky->r += 2*M_PI;
+            while (bSky->r > 2*M_PI)
+                bSky->r -= 2*M_PI;
+            while (bSky->r <      0)
+                bSky->r += 2*M_PI;
+
+            ok_float(aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, aSky->r*PM_DEG_RAD - bSky->r*PM_DEG_RAD);
+            ok_float(aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, aSky->d*PM_DEG_RAD - bSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (aSky);
+    psFree (aTPA);
+    psFree (aFPA);
+    psFree (aChip);
+
+    psFree (bSky);
+    psFree (bChip);
+
+    psFree (wcs);
+    psFree (header2);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header1);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test1x()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 20.0;
+    coords.crpix2 = 50.0;
+    coords.cdelt1 = 1.0/3600.0;
+    coords.cdelt2 = 1.0/3600.0;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header1 = WriteCoordsToHeader (&coords);
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header1, 10.0);
+
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psMetadata *header2 = psMetadataAlloc();
+    status = pmAstromWriteWCS (header2, fpa, chip, 0.001);
+    pmAstromWCS *wcs = pmAstromWCSfromHeader(header2);
+
+    psPlane  *aChip = psPlaneAlloc();
+    psPlane  *aFPA = psPlaneAlloc();
+    psPlane  *aTPA = psPlaneAlloc();
+    psSphere *aSky = psSphereAlloc();
+
+    psPlane  *bChip = psPlaneAlloc();
+    psSphere *bSky = psSphereAlloc();
+
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            aChip->x = x;
+            aChip->y = y;
+            bChip->x = x;
+            bChip->y = y;
+
+            psPlaneTransformApply (aFPA, chip->toFPA, aChip);
+            psPlaneTransformApply (aTPA, fpa->toTPA, aFPA);
+            psDeproject (aSky, aTPA, fpa->toSky);
+
+            pmAstromWCStoSky (bSky, wcs, bChip);
+
+            while (aSky->r > 2*M_PI)
+                aSky->r -= 2*M_PI;
+            while (aSky->r <      0)
+                aSky->r += 2*M_PI;
+            while (bSky->r > 2*M_PI)
+                bSky->r -= 2*M_PI;
+            while (bSky->r <      0)
+                bSky->r += 2*M_PI;
+
+            ok_float(aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, aSky->r*PM_DEG_RAD - bSky->r*PM_DEG_RAD);
+            ok_float(aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, aSky->d*PM_DEG_RAD - bSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (aSky);
+    psFree (aTPA);
+    psFree (aFPA);
+    psFree (aChip);
+
+    psFree (bSky);
+    psFree (bChip);
+
+    psFree (wcs);
+    psFree (header2);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header1);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test2x()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 50.0;
+    coords.crpix2 = -20.0;
+    coords.cdelt1 = 1.0/3600.0;
+    coords.cdelt2 = 1.0/3600.0;
+    coords.pc1_1  = 0.9;
+    coords.pc1_2  = 0.1;
+    coords.pc2_1  = -0.1;
+    coords.pc2_2  = 0.9;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    psMetadata *header1 = WriteCoordsToHeader (&coords);
+
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header1, 10.0);
+
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psMetadata *header2 = psMetadataAlloc();
+    status = pmAstromWriteWCS (header2, fpa, chip, 0.001);
+
+    pmAstromWCS *wcs = pmAstromWCSfromHeader(header2);
+
+    psPlane  *aChip = psPlaneAlloc();
+    psPlane  *aFPA = psPlaneAlloc();
+    psPlane  *aTPA = psPlaneAlloc();
+    psSphere *aSky = psSphereAlloc();
+
+    psPlane  *bChip = psPlaneAlloc();
+    psSphere *bSky = psSphereAlloc();
+
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            aChip->x = x;
+            aChip->y = y;
+            bChip->x = x;
+            bChip->y = y;
+
+            psPlaneTransformApply (aFPA, chip->toFPA, aChip);
+            psPlaneTransformApply (aTPA, fpa->toTPA, aFPA);
+            psDeproject (aSky, aTPA, fpa->toSky);
+
+            pmAstromWCStoSky (bSky, wcs, bChip);
+
+            while (aSky->r > 2*M_PI)
+                aSky->r -= 2*M_PI;
+            while (aSky->r <      0)
+                aSky->r += 2*M_PI;
+            while (bSky->r > 2*M_PI)
+                bSky->r -= 2*M_PI;
+            while (bSky->r <      0)
+                bSky->r += 2*M_PI;
+
+            ok_float(aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, aSky->r*PM_DEG_RAD - bSky->r*PM_DEG_RAD);
+            ok_float(aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, "coordinate match: %f vs %f (delta = %f)", aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, aSky->d*PM_DEG_RAD - bSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (aSky);
+    psFree (aTPA);
+    psFree (aFPA);
+    psFree (aChip);
+
+    psFree (bSky);
+    psFree (bChip);
+
+    psFree (wcs);
+    psFree (header2);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header1);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+void test3x()
+{
+    note("test pmAstromReadWCS");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style coordinate system
+    Coords coords;
+    strcpy (coords.ctype, "RA---TAN");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 20.0;
+    coords.crpix2 = 50.0;
+    coords.cdelt1 = 1.0/3600.0;
+    coords.cdelt2 = 1.0/3600.0;
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 2;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+    coords.polyterms[0][0] = 0.01; // L vs X^2
+    coords.polyterms[2][1] = 0.01; // M vs Y^2
+
+    psMetadata *header1 = WriteCoordsToHeader (&coords);
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadWCS (fpa, chip, header1, 10.0);
+
+    ok (status, "converted WCS keywords to WCS astrometry");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psMetadata *header2 = psMetadataAlloc();
+    status = pmAstromWriteWCS (header2, fpa, chip, 0.00001);
+
+    pmAstromWCS *wcs = pmAstromWCSfromHeader(header2);
+
+    psPlane  *aChip = psPlaneAlloc();
+    psPlane  *aFPA = psPlaneAlloc();
+    psPlane  *aTPA = psPlaneAlloc();
+    psSphere *aSky = psSphereAlloc();
+
+    psPlane  *bChip = psPlaneAlloc();
+    psSphere *bSky = psSphereAlloc();
+
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            aChip->x = x;
+            aChip->y = y;
+            bChip->x = x;
+            bChip->y = y;
+
+            psPlaneTransformApply (aFPA, chip->toFPA, aChip);
+            psPlaneTransformApply (aTPA, fpa->toTPA, aFPA);
+            psDeproject (aSky, aTPA, fpa->toSky);
+
+            pmAstromWCStoSky (bSky, wcs, bChip);
+
+            while (aSky->r > 2*M_PI)
+                aSky->r -= 2*M_PI;
+            while (aSky->r <      0)
+                aSky->r += 2*M_PI;
+            while (bSky->r > 2*M_PI)
+                bSky->r -= 2*M_PI;
+            while (bSky->r <      0)
+                bSky->r += 2*M_PI;
+
+            // XXX we are getting round-off errors as a result of the wcs transformation
+            // having terms in units of pix/degree. for now require 10mas on this
+            ok_float_tol(aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, 0.01/3600.0, "coordinate match: %f vs %f (delta = %f)", aSky->r*PM_DEG_RAD, bSky->r*PM_DEG_RAD, aSky->r*PM_DEG_RAD - bSky->r*PM_DEG_RAD);
+            ok_float_tol(aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, 0.01/3600.0, "coordinate match: %f vs %f (delta = %f)", aSky->d*PM_DEG_RAD, bSky->d*PM_DEG_RAD, aSky->d*PM_DEG_RAD - bSky->d*PM_DEG_RAD);
+        }
+    }
+    psFree (aSky);
+    psFree (aTPA);
+    psFree (aFPA);
+    psFree (aChip);
+
+    psFree (bSky);
+    psFree (bChip);
+
+    psFree (wcs);
+    psFree (header2);
+
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+    psFree (header1);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+psMetadata *WriteCoordsToHeader (Coords *coords)
+{
+
+    char name[16];
+
+    // construct a header using coords as the input
+    psMetadata *header = psMetadataAlloc();
+
+    sprintf (name, "RA--%s", &coords[0].ctype[4]);
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE1", PS_META_REPLACE, "", name);
+    sprintf (name, "DEC-%s", &coords[0].ctype[4]);
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE2", PS_META_REPLACE, "", name);
+
+    // center coords (R,D)
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRVAL1", PS_META_REPLACE, "", coords[0].crval1);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRVAL2", PS_META_REPLACE, "", coords[0].crval2);
+
+    // center coords (X,Y)
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRPIX1", PS_META_REPLACE, "", coords[0].crpix1);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRPIX2", PS_META_REPLACE, "", coords[0].crpix2);
+
+    // degrees per pixel
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CDELT1",  PS_META_REPLACE, "", coords[0].cdelt1);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CDELT2",  PS_META_REPLACE, "", coords[0].cdelt2);
+
+    // rotation matrix
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC001001", PS_META_REPLACE, "", coords[0].pc1_1);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC001002", PS_META_REPLACE, "", coords[0].pc1_2);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC002001", PS_META_REPLACE, "", coords[0].pc2_1);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC002002", PS_META_REPLACE, "", coords[0].pc2_2);
+
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA1X2Y0", PS_META_REPLACE, "", coords[0].polyterms[0][0]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA1X1Y1", PS_META_REPLACE, "", coords[0].polyterms[1][0]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA1X0Y2", PS_META_REPLACE, "", coords[0].polyterms[2][0]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA2X2Y0", PS_META_REPLACE, "", coords[0].polyterms[0][1]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA2X1Y1", PS_META_REPLACE, "", coords[0].polyterms[1][1]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA2X0Y2", PS_META_REPLACE, "", coords[0].polyterms[2][1]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA1X3Y0", PS_META_REPLACE, "", coords[0].polyterms[3][0]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA1X2Y1", PS_META_REPLACE, "", coords[0].polyterms[4][0]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA1X1Y2", PS_META_REPLACE, "", coords[0].polyterms[5][0]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA1X0Y3", PS_META_REPLACE, "", coords[0].polyterms[6][0]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA2X3Y0", PS_META_REPLACE, "", coords[0].polyterms[3][1]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA2X2Y1", PS_META_REPLACE, "", coords[0].polyterms[4][1]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA2X1Y2", PS_META_REPLACE, "", coords[0].polyterms[5][1]);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PCA2X0Y3", PS_META_REPLACE, "", coords[0].polyterms[6][1]);
+
+    psMetadataAddS32 (header, PS_LIST_TAIL, "NPLYTERM", PS_META_REPLACE, "", coords[0].Npolyterms);
+
+    return header;
+}
+
+# else
+
+    int main (void)
+{
+    plan_tests(0);
+
+    note("pmAstrometryWCS tests compared with DVO coords routines : SKIPPED (libdvo not available)");
+
+    return exit_status();
+}
+
+# endif
+
Index: /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO4.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO4.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/astrom/tap_pmAstrometryWCS_DVO4.c	(revision 15358)
@@ -0,0 +1,955 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+// HAVA_KAPA checks for Ohana libraries, needed for the Coords structure
+# if (HAVE_KAPA)
+# include "dvo.h"
+
+psMetadata *WriteCoordsToHeader (Coords *coords);
+void test1(); // basic WRP+DIS projections,
+void test2(); // small rotation
+void test3(); // 2nd order terms in WRP
+void test4(); // small rotation in WRP and DIS
+void test5(); // 2nd order terms in WRP and DIS
+void test1x(); // basic WRP+DIS projections with central offset
+void test2x(); // small rotation with central offset
+void test3x(); // 2nd order term in WRP with central offset
+
+int main (void)
+{
+    plan_tests(1329);
+
+    note("pmAstromWriteWCS tests compared with DVO coords routines");
+
+    test1();
+    test2();
+    test3();
+    test4();
+    test5();
+    test1x();
+    test2x();
+    test3x();
+
+    return exit_status();
+}
+
+// create a fake chip-level mosaic header
+void test1()
+{
+    note("test pmAstrom Read,Write BilevelChip");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style mosaic coordinate system
+    // chip-level data (chip -> fpa)
+    Coords coords;
+    strcpy (coords.ctype, "RA---WRP");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 10.0; // microns per pixel
+    coords.cdelt2 = 10.0; // microns per pixel
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    // mosaic-level data (fpa->sky)
+    Coords mosaic;
+    strcpy (mosaic.ctype, "RA---DIS");
+    mosaic.crval1 = 0.0;
+    mosaic.crval2 = 0.0;
+    mosaic.crpix1 = 0.0;
+    mosaic.crpix2 = 0.0;
+    mosaic.cdelt1 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.cdelt2 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.pc1_1  = 1.0;
+    mosaic.pc1_2  = 0.0;
+    mosaic.pc2_1  = 0.0;
+    mosaic.pc2_2  = 1.0;
+    mosaic.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        mosaic.polyterms[i][0] = 0.0;
+        mosaic.polyterms[i][1] = 0.0;
+    }
+    RegisterMosaic (&mosaic);
+
+    psMetadata *headerChp = WriteCoordsToHeader (&coords);
+    psMetadata *headerMos = WriteCoordsToHeader (&mosaic);
+
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadBilevelChip (chip, headerChp);
+    ok (status, "read bilevel chip header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    bool status = pmAstromReadBilevelMosaic (fpa, headerMos);
+    ok (status, "read bilevel fpa header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane  *onChip = psPlaneAlloc();
+    psPlane  *onFPA  = psPlaneAlloc();
+    psPlane  *onTPA  = psPlaneAlloc();
+    psSphere *onSky  = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            // fprintf (stderr, "fpa x: %f vs %f : %f\n", rDVO, onFPA->x, rDVO - onFPA->x);
+            // fprintf (stderr, "fpa y: %f vs %f : %f\n", dDVO, onFPA->y, dDVO - onFPA->y);
+
+            ok_float(onSky->r*PM_DEG_RAD, rDVO, "coordinate match: %f vs %f (delta = %f)", onSky->r*PM_DEG_RAD, rDVO, 3600.0*(onSky->r*PM_DEG_RAD - rDVO));
+            ok_float(onSky->d*PM_DEG_RAD, dDVO, "coordinate match: %f vs %f (delta = %f)", onSky->d*PM_DEG_RAD, dDVO, 3600.0*(onSky->d*PM_DEG_RAD - dDVO));
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+
+    psFree (headerMos);
+    psFree (headerChp);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+// create a fake chip-level mosaic header
+void test2()
+{
+    note("test pmAstrom Read,Write BilevelChip");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style mosaic coordinate system
+    // chip-level data (chip -> fpa)
+    Coords coords;
+    strcpy (coords.ctype, "RA---WRP");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 10.0; // microns per pixel
+    coords.cdelt2 = 10.0; // microns per pixel
+    coords.pc1_1  = 0.9;
+    coords.pc1_2  = 0.1;
+    coords.pc2_1  =-0.1;
+    coords.pc2_2  = 0.9;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    // mosaic-level data (fpa->sky)
+    Coords mosaic;
+    strcpy (mosaic.ctype, "RA---DIS");
+    mosaic.crval1 = 0.0;
+    mosaic.crval2 = 0.0;
+    mosaic.crpix1 = 0.0;
+    mosaic.crpix2 = 0.0;
+    mosaic.cdelt1 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.cdelt2 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.pc1_1  = 1.0;
+    mosaic.pc1_2  = 0.0;
+    mosaic.pc2_1  = 0.0;
+    mosaic.pc2_2  = 1.0;
+    mosaic.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        mosaic.polyterms[i][0] = 0.0;
+        mosaic.polyterms[i][1] = 0.0;
+    }
+    RegisterMosaic (&mosaic);
+
+    psMetadata *headerChp = WriteCoordsToHeader (&coords);
+    psMetadata *headerMos = WriteCoordsToHeader (&mosaic);
+
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadBilevelChip (chip, headerChp);
+    ok (status, "read bilevel chip header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    bool status = pmAstromReadBilevelMosaic (fpa, headerMos);
+    ok (status, "read bilevel fpa header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane  *onChip = psPlaneAlloc();
+    psPlane  *onFPA  = psPlaneAlloc();
+    psPlane  *onTPA  = psPlaneAlloc();
+    psSphere *onSky  = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            // fprintf (stderr, "fpa x: %f vs %f : %f\n", rDVO, onFPA->x, rDVO - onFPA->x);
+            // fprintf (stderr, "fpa y: %f vs %f : %f\n", dDVO, onFPA->y, dDVO - onFPA->y);
+
+            ok_float(onSky->r*PM_DEG_RAD, rDVO, "coordinate match: %f vs %f (delta = %f)", onSky->r*PM_DEG_RAD, rDVO, 3600.0*(onSky->r*PM_DEG_RAD - rDVO));
+            ok_float(onSky->d*PM_DEG_RAD, dDVO, "coordinate match: %f vs %f (delta = %f)", onSky->d*PM_DEG_RAD, dDVO, 3600.0*(onSky->d*PM_DEG_RAD - dDVO));
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+
+    psFree (headerMos);
+    psFree (headerChp);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+// create a fake chip-level mosaic header
+void test3()
+{
+    note("test pmAstrom Read,Write BilevelChip");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style mosaic coordinate system
+    // chip-level data (chip -> fpa)
+    Coords coords;
+    strcpy (coords.ctype, "RA---WRP");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 10.0; // microns per pixel
+    coords.cdelt2 = 10.0; // microns per pixel
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 2;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+    coords.polyterms[0][0] = 0.01; // L vs X^2
+    coords.polyterms[2][1] = 0.01; // M vs Y^2
+
+    // mosaic-level data (fpa->sky)
+    Coords mosaic;
+    strcpy (mosaic.ctype, "RA---DIS");
+    mosaic.crval1 = 0.0;
+    mosaic.crval2 = 0.0;
+    mosaic.crpix1 = 0.0;
+    mosaic.crpix2 = 0.0;
+    mosaic.cdelt1 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.cdelt2 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.pc1_1  = 1.0;
+    mosaic.pc1_2  = 0.0;
+    mosaic.pc2_1  = 0.0;
+    mosaic.pc2_2  = 1.0;
+    mosaic.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        mosaic.polyterms[i][0] = 0.0;
+        mosaic.polyterms[i][1] = 0.0;
+    }
+    RegisterMosaic (&mosaic);
+
+    psMetadata *headerChp = WriteCoordsToHeader (&coords);
+    psMetadata *headerMos = WriteCoordsToHeader (&mosaic);
+
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadBilevelChip (chip, headerChp);
+    ok (status, "read bilevel chip header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    bool status = pmAstromReadBilevelMosaic (fpa, headerMos);
+    ok (status, "read bilevel fpa header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane  *onChip = psPlaneAlloc();
+    psPlane  *onFPA  = psPlaneAlloc();
+    psPlane  *onTPA  = psPlaneAlloc();
+    psSphere *onSky  = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            // fprintf (stderr, "fpa x: %f vs %f : %f\n", rDVO, onFPA->x, rDVO - onFPA->x);
+            // fprintf (stderr, "fpa y: %f vs %f : %f\n", dDVO, onFPA->y, dDVO - onFPA->y);
+
+            ok_float(onSky->r*PM_DEG_RAD, rDVO, "coordinate match: %f vs %f (delta = %f)", onSky->r*PM_DEG_RAD, rDVO, 3600.0*(onSky->r*PM_DEG_RAD - rDVO));
+            ok_float(onSky->d*PM_DEG_RAD, dDVO, "coordinate match: %f vs %f (delta = %f)", onSky->d*PM_DEG_RAD, dDVO, 3600.0*(onSky->d*PM_DEG_RAD - dDVO));
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+
+    psFree (headerMos);
+    psFree (headerChp);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+// create a fake chip-level mosaic header
+void test4()
+{
+    note("test pmAstrom Read,Write BilevelChip");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style mosaic coordinate system
+    // chip-level data (chip -> fpa)
+    Coords coords;
+    strcpy (coords.ctype, "RA---WRP");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 10.0; // microns per pixel
+    coords.cdelt2 = 10.0; // microns per pixel
+    coords.pc1_1  = 0.9;
+    coords.pc1_2  = 0.1;
+    coords.pc2_1  =-0.1;
+    coords.pc2_2  = 0.9;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    // mosaic-level data (fpa->sky)
+    Coords mosaic;
+    strcpy (mosaic.ctype, "RA---DIS");
+    mosaic.crval1 = 0.0;
+    mosaic.crval2 = 0.0;
+    mosaic.crpix1 = 0.0;
+    mosaic.crpix2 = 0.0;
+    mosaic.cdelt1 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.cdelt2 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.pc1_1  = 0.9;
+    mosaic.pc1_2  =-0.1;
+    mosaic.pc2_1  = 0.1;
+    mosaic.pc2_2  = 0.9;
+    mosaic.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        mosaic.polyterms[i][0] = 0.0;
+        mosaic.polyterms[i][1] = 0.0;
+    }
+    RegisterMosaic (&mosaic);
+
+    psMetadata *headerChp = WriteCoordsToHeader (&coords);
+    psMetadata *headerMos = WriteCoordsToHeader (&mosaic);
+
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadBilevelChip (chip, headerChp);
+    ok (status, "read bilevel chip header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    bool status = pmAstromReadBilevelMosaic (fpa, headerMos);
+    ok (status, "read bilevel fpa header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane  *onChip = psPlaneAlloc();
+    psPlane  *onFPA  = psPlaneAlloc();
+    psPlane  *onTPA  = psPlaneAlloc();
+    psSphere *onSky  = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            // fprintf (stderr, "fpa x: %f vs %f : %f\n", rDVO, onFPA->x, rDVO - onFPA->x);
+            // fprintf (stderr, "fpa y: %f vs %f : %f\n", dDVO, onFPA->y, dDVO - onFPA->y);
+
+            ok_float(onSky->r*PM_DEG_RAD, rDVO, "coordinate match: %f vs %f (delta = %f)", onSky->r*PM_DEG_RAD, rDVO, 3600.0*(onSky->r*PM_DEG_RAD - rDVO));
+            ok_float(onSky->d*PM_DEG_RAD, dDVO, "coordinate match: %f vs %f (delta = %f)", onSky->d*PM_DEG_RAD, dDVO, 3600.0*(onSky->d*PM_DEG_RAD - dDVO));
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+
+    psFree (headerMos);
+    psFree (headerChp);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+// create a fake chip-level mosaic header
+void test5()
+{
+    note("test pmAstrom Read,Write BilevelChip");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style mosaic coordinate system
+    // chip-level data (chip -> fpa)
+    Coords coords;
+    strcpy (coords.ctype, "RA---WRP");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = 0.0;
+    coords.crpix2 = 0.0;
+    coords.cdelt1 = 10.0; // microns per pixel
+    coords.cdelt2 = 10.0; // microns per pixel
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 2;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+    coords.polyterms[0][0] = 0.01; // L vs X^2
+    coords.polyterms[2][1] = 0.01; // M vs Y^2
+
+    // mosaic-level data (fpa->sky)
+    Coords mosaic;
+    strcpy (mosaic.ctype, "RA---DIS");
+    mosaic.crval1 = 0.0;
+    mosaic.crval2 = 0.0;
+    mosaic.crpix1 = 0.0;
+    mosaic.crpix2 = 0.0;
+    mosaic.cdelt1 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.cdelt2 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.pc1_1  = 1.0;
+    mosaic.pc1_2  = 0.0;
+    mosaic.pc2_1  = 0.0;
+    mosaic.pc2_2  = 1.0;
+    mosaic.Npolyterms = 2;
+    for (int i = 0; i < 7; i++) {
+        mosaic.polyterms[i][0] = 0.0;
+        mosaic.polyterms[i][1] = 0.0;
+    }
+    RegisterMosaic (&mosaic);
+    mosaic.polyterms[0][0] = 0.01; // L vs X^2
+    mosaic.polyterms[2][1] = 0.01; // M vs Y^2
+
+    psMetadata *headerChp = WriteCoordsToHeader (&coords);
+    psMetadata *headerMos = WriteCoordsToHeader (&mosaic);
+
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadBilevelChip (chip, headerChp);
+    ok (status, "read bilevel chip header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    bool status = pmAstromReadBilevelMosaic (fpa, headerMos);
+    ok (status, "read bilevel fpa header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane  *onChip = psPlaneAlloc();
+    psPlane  *onFPA  = psPlaneAlloc();
+    psPlane  *onTPA  = psPlaneAlloc();
+    psSphere *onSky  = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            // fprintf (stderr, "fpa x: %f vs %f : %f\n", rDVO, onFPA->x, rDVO - onFPA->x);
+            // fprintf (stderr, "fpa y: %f vs %f : %f\n", dDVO, onFPA->y, dDVO - onFPA->y);
+
+            ok_float(onSky->r*PM_DEG_RAD, rDVO, "coordinate match: %f vs %f (delta = %f)", onSky->r*PM_DEG_RAD, rDVO, 3600.0*(onSky->r*PM_DEG_RAD - rDVO));
+            ok_float(onSky->d*PM_DEG_RAD, dDVO, "coordinate match: %f vs %f (delta = %f)", onSky->d*PM_DEG_RAD, dDVO, 3600.0*(onSky->d*PM_DEG_RAD - dDVO));
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+
+    psFree (headerMos);
+    psFree (headerChp);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+// create a fake chip-level mosaic header
+void test1x()
+{
+    note("test pmAstrom Read,Write BilevelChip");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style mosaic coordinate system
+    // chip-level data (chip -> fpa)
+    Coords coords;
+    strcpy (coords.ctype, "RA---WRP");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = +50.0;
+    coords.crpix2 = -20.0;
+    coords.cdelt1 = 10.0; // microns per pixel
+    coords.cdelt2 = 10.0; // microns per pixel
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    // mosaic-level data (fpa->sky)
+    Coords mosaic;
+    strcpy (mosaic.ctype, "RA---DIS");
+    mosaic.crval1 = 0.0;
+    mosaic.crval2 = 0.0;
+    mosaic.crpix1 = 0.0;
+    mosaic.crpix2 = 0.0;
+    mosaic.cdelt1 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.cdelt2 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.pc1_1  = 1.0;
+    mosaic.pc1_2  = 0.0;
+    mosaic.pc2_1  = 0.0;
+    mosaic.pc2_2  = 1.0;
+    mosaic.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        mosaic.polyterms[i][0] = 0.0;
+        mosaic.polyterms[i][1] = 0.0;
+    }
+    RegisterMosaic (&mosaic);
+
+    psMetadata *headerChp = WriteCoordsToHeader (&coords);
+    psMetadata *headerMos = WriteCoordsToHeader (&mosaic);
+
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadBilevelChip (chip, headerChp);
+    ok (status, "read bilevel chip header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    bool status = pmAstromReadBilevelMosaic (fpa, headerMos);
+    ok (status, "read bilevel fpa header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane  *onChip = psPlaneAlloc();
+    psPlane  *onFPA  = psPlaneAlloc();
+    psPlane  *onTPA  = psPlaneAlloc();
+    psSphere *onSky  = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            // fprintf (stderr, "fpa x: %f vs %f : %f\n", rDVO, onFPA->x, rDVO - onFPA->x);
+            // fprintf (stderr, "fpa y: %f vs %f : %f\n", dDVO, onFPA->y, dDVO - onFPA->y);
+
+            ok_float(onSky->r*PM_DEG_RAD, rDVO, "coordinate match: %f vs %f (delta = %f)", onSky->r*PM_DEG_RAD, rDVO, 3600.0*(onSky->r*PM_DEG_RAD - rDVO));
+            ok_float(onSky->d*PM_DEG_RAD, dDVO, "coordinate match: %f vs %f (delta = %f)", onSky->d*PM_DEG_RAD, dDVO, 3600.0*(onSky->d*PM_DEG_RAD - dDVO));
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+
+    psFree (headerMos);
+    psFree (headerChp);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+// create a fake chip-level mosaic header
+void test2x()
+{
+    note("test pmAstrom Read,Write BilevelChip");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style mosaic coordinate system
+    // chip-level data (chip -> fpa)
+    Coords coords;
+    strcpy (coords.ctype, "RA---WRP");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = +50.0;
+    coords.crpix2 = -20.0;
+    coords.cdelt1 = 10.0; // microns per pixel
+    coords.cdelt2 = 10.0; // microns per pixel
+    coords.pc1_1  = 0.9;
+    coords.pc1_2  = 0.1;
+    coords.pc2_1  =-0.1;
+    coords.pc2_2  = 0.9;
+    coords.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+
+    // mosaic-level data (fpa->sky)
+    Coords mosaic;
+    strcpy (mosaic.ctype, "RA---DIS");
+    mosaic.crval1 = 0.0;
+    mosaic.crval2 = 0.0;
+    mosaic.crpix1 = 0.0;
+    mosaic.crpix2 = 0.0;
+    mosaic.cdelt1 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.cdelt2 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.pc1_1  = 1.0;
+    mosaic.pc1_2  = 0.0;
+    mosaic.pc2_1  = 0.0;
+    mosaic.pc2_2  = 1.0;
+    mosaic.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        mosaic.polyterms[i][0] = 0.0;
+        mosaic.polyterms[i][1] = 0.0;
+    }
+    RegisterMosaic (&mosaic);
+
+    psMetadata *headerChp = WriteCoordsToHeader (&coords);
+    psMetadata *headerMos = WriteCoordsToHeader (&mosaic);
+
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadBilevelChip (chip, headerChp);
+    ok (status, "read bilevel chip header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    bool status = pmAstromReadBilevelMosaic (fpa, headerMos);
+    ok (status, "read bilevel fpa header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane  *onChip = psPlaneAlloc();
+    psPlane  *onFPA  = psPlaneAlloc();
+    psPlane  *onTPA  = psPlaneAlloc();
+    psSphere *onSky  = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            // fprintf (stderr, "fpa x: %f vs %f : %f\n", rDVO, onFPA->x, rDVO - onFPA->x);
+            // fprintf (stderr, "fpa y: %f vs %f : %f\n", dDVO, onFPA->y, dDVO - onFPA->y);
+
+            ok_float(onSky->r*PM_DEG_RAD, rDVO, "coordinate match: %f vs %f (delta = %f)", onSky->r*PM_DEG_RAD, rDVO, 3600.0*(onSky->r*PM_DEG_RAD - rDVO));
+            ok_float(onSky->d*PM_DEG_RAD, dDVO, "coordinate match: %f vs %f (delta = %f)", onSky->d*PM_DEG_RAD, dDVO, 3600.0*(onSky->d*PM_DEG_RAD - dDVO));
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+
+    psFree (headerMos);
+    psFree (headerChp);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+// create a fake chip-level mosaic header
+void test3x()
+{
+    note("test pmAstrom Read,Write BilevelChip");
+    psMemId id = psMemGetId();
+
+    // build a DVO-style mosaic coordinate system
+    // chip-level data (chip -> fpa)
+    Coords coords;
+    strcpy (coords.ctype, "RA---WRP");
+    coords.crval1 = 0.0;
+    coords.crval2 = 0.0;
+    coords.crpix1 = +50.0;
+    coords.crpix2 = -20.0;
+    coords.cdelt1 = 10.0; // microns per pixel
+    coords.cdelt2 = 10.0; // microns per pixel
+    coords.pc1_1  = 1.0;
+    coords.pc1_2  = 0.0;
+    coords.pc2_1  = 0.0;
+    coords.pc2_2  = 1.0;
+    coords.Npolyterms = 2;
+    for (int i = 0; i < 7; i++) {
+        coords.polyterms[i][0] = 0.0;
+        coords.polyterms[i][1] = 0.0;
+    }
+    coords.polyterms[0][0] = 0.01; // L vs X^2
+    coords.polyterms[2][1] = 0.01; // M vs Y^2
+
+    // mosaic-level data (fpa->sky)
+    Coords mosaic;
+    strcpy (mosaic.ctype, "RA---DIS");
+    mosaic.crval1 = 0.0;
+    mosaic.crval2 = 0.0;
+    mosaic.crpix1 = 0.0;
+    mosaic.crpix2 = 0.0;
+    mosaic.cdelt1 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.cdelt2 = 1.0 / 10.0 / 3600.0; // degrees per micron
+    mosaic.pc1_1  = 1.0;
+    mosaic.pc1_2  = 0.0;
+    mosaic.pc2_1  = 0.0;
+    mosaic.pc2_2  = 1.0;
+    mosaic.Npolyterms = 0;
+    for (int i = 0; i < 7; i++) {
+        mosaic.polyterms[i][0] = 0.0;
+        mosaic.polyterms[i][1] = 0.0;
+    }
+    RegisterMosaic (&mosaic);
+
+    psMetadata *headerChp = WriteCoordsToHeader (&coords);
+    psMetadata *headerMos = WriteCoordsToHeader (&mosaic);
+
+    pmFPA *fpa = pmFPAAlloc (NULL);
+    pmChip *chip = pmChipAlloc (fpa, NULL);
+
+    bool status = pmAstromReadBilevelChip (chip, headerChp);
+    ok (status, "read bilevel chip header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    bool status = pmAstromReadBilevelMosaic (fpa, headerMos);
+    ok (status, "read bilevel fpa header");
+    skip_start (!status, 1, "*** WCS Conversion FAILS *** : skipping related tests");
+
+    psPlane  *onChip = psPlaneAlloc();
+    psPlane  *onFPA  = psPlaneAlloc();
+    psPlane  *onTPA  = psPlaneAlloc();
+    psSphere *onSky  = psSphereAlloc();
+
+    double rDVO, dDVO;
+    for (double x = -2000; x <= +2000; x+= 500.0) {
+        for (double y = -2000; y <= +2000; y+= 500.0) {
+            XY_to_RD (&rDVO, &dDVO, x, y, &coords);
+
+            onChip->x = x;
+            onChip->y = y;
+
+            psPlaneTransformApply (onFPA, chip->toFPA, onChip);
+            psPlaneTransformApply (onTPA, fpa->toTPA, onFPA);
+            psDeproject (onSky, onTPA, fpa->toSky);
+
+            while (onSky->r > 2*M_PI)
+                onSky->r -= 2*M_PI;
+            while (onSky->r <      0)
+                onSky->r += 2*M_PI;
+
+            // fprintf (stderr, "fpa x: %f vs %f : %f\n", rDVO, onFPA->x, rDVO - onFPA->x);
+            // fprintf (stderr, "fpa y: %f vs %f : %f\n", dDVO, onFPA->y, dDVO - onFPA->y);
+
+            ok_float(onSky->r*PM_DEG_RAD, rDVO, "coordinate match: %f vs %f (delta = %f)", onSky->r*PM_DEG_RAD, rDVO, 3600.0*(onSky->r*PM_DEG_RAD - rDVO));
+            ok_float(onSky->d*PM_DEG_RAD, dDVO, "coordinate match: %f vs %f (delta = %f)", onSky->d*PM_DEG_RAD, dDVO, 3600.0*(onSky->d*PM_DEG_RAD - dDVO));
+        }
+    }
+    psFree (onSky);
+    psFree (onTPA);
+    psFree (onFPA);
+    psFree (onChip);
+
+    skip_end();
+    skip_end();
+    psFree (fpa);
+    psFree (chip);
+
+    psFree (headerMos);
+    psFree (headerChp);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+}
+
+psMetadata *WriteCoordsToHeader (Coords *coords)
+{
+
+    char name[16];
+
+    // construct a header using coords as the input
+    psMetadata *header = psMetadataAlloc();
+
+    sprintf (name, "RA--%s", &coords[0].ctype[4]);
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE1", PS_META_REPLACE, "", name);
+    sprintf (name, "DEC-%s", &coords[0].ctype[4]);
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE2", PS_META_REPLACE, "", name);
+
+    // center coords (R,D)
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRVAL1", PS_META_REPLACE, "", coords[0].crval1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRVAL2", PS_META_REPLACE, "", coords[0].crval2);
+
+    // center coords (X,Y)
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRPIX1", PS_META_REPLACE, "", coords[0].crpix1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CRPIX2", PS_META_REPLACE, "", coords[0].crpix2);
+
+    // degrees per pixel
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CDELT1",  PS_META_REPLACE, "", coords[0].cdelt1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "CDELT2",  PS_META_REPLACE, "", coords[0].cdelt2);
+
+    // rotation matrix
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC001001", PS_META_REPLACE, "", coords[0].pc1_1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC001002", PS_META_REPLACE, "", coords[0].pc1_2);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC002001", PS_META_REPLACE, "", coords[0].pc2_1);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PC002002", PS_META_REPLACE, "", coords[0].pc2_2);
+
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X2Y0", PS_META_REPLACE, "", coords[0].polyterms[0][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X1Y1", PS_META_REPLACE, "", coords[0].polyterms[1][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X0Y2", PS_META_REPLACE, "", coords[0].polyterms[2][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X2Y0", PS_META_REPLACE, "", coords[0].polyterms[0][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X1Y1", PS_META_REPLACE, "", coords[0].polyterms[1][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X0Y2", PS_META_REPLACE, "", coords[0].polyterms[2][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X3Y0", PS_META_REPLACE, "", coords[0].polyterms[3][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X2Y1", PS_META_REPLACE, "", coords[0].polyterms[4][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X1Y2", PS_META_REPLACE, "", coords[0].polyterms[5][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA1X0Y3", PS_META_REPLACE, "", coords[0].polyterms[6][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X3Y0", PS_META_REPLACE, "", coords[0].polyterms[3][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X2Y1", PS_META_REPLACE, "", coords[0].polyterms[4][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X1Y2", PS_META_REPLACE, "", coords[0].polyterms[5][1]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "PCA2X0Y3", PS_META_REPLACE, "", coords[0].polyterms[6][1]);
+
+    psMetadataAddS32 (header, PS_LIST_TAIL, "NPLYTERM", PS_META_REPLACE, "", coords[0].Npolyterms);
+
+    return header;
+}
+
+# else
+
+    int main (void)
+{
+    plan_tests(0);
+
+    note("pmAstrometryWCS tests compared with DVO coords routines : SKIPPED (libdvo not available)");
+
+    return exit_status();
+}
+
+# endif
+
Index: /branches/eam_branch_20071023/psModules/test/astrom/tap_pmHDU.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/astrom/tap_pmHDU.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/astrom/tap_pmHDU.c	(revision 15358)
@@ -0,0 +1,272 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+//#include <unistd.h>
+
+#define GENIMAGE(img,c,r,TYP, valueFcn) \
+img = psImageAlloc(c,r,PS_TYPE_##TYP); \
+for (psU32 row=0;row<r;row++) { \
+    ps##TYP* imgRow = img->data.TYP[row]; \
+    for (psU32 col=0;col<c;col++) { \
+        imgRow[col] = (ps##TYP)(valueFcn); \
+    } \
+}
+const char* fitsFilename = "tmp.fits";
+const char* fitsFilename2 = "tmp2.fits";
+
+bool createFitsFile(void)
+{
+    psFits* fitsFile = psFitsOpen(fitsFilename, "w");
+
+    if (fitsFile == NULL) {
+        diag("Could not create 'multi' FITS file");
+        return false;
+    }
+
+    psImage* image = psImageAlloc(16, 16, PS_TYPE_F32);
+
+    char extname[80];
+    for (int lcv = 0; lcv < 8; lcv++) {
+        snprintf(extname, 80, "ext-%d", lcv);
+
+        psMetadata* header = psMetadataAlloc();
+
+        psMetadataAdd(header, PS_LIST_TAIL, "MYINT",
+                      PS_DATA_S32,
+                      "psS32 Item", (psS32)lcv);
+
+        psMetadataAdd(header, PS_LIST_TAIL, "MYFLT",
+                      PS_DATA_F32,
+                      "psF32 Item", (float)(1.0f/(float)(1+lcv)));
+
+        psMetadataAdd(header, PS_LIST_TAIL, "MYDBL",
+                      PS_DATA_F64,
+                      "psF64 Item", (double)(1.0/(double)(1+lcv)));
+
+        psMetadataAdd(header, PS_LIST_TAIL, "MYBOOL",
+                      PS_DATA_BOOL,
+                      "psBool Item",
+                      (lcv%2 == 0));
+
+        psMetadataAdd(header, PS_LIST_TAIL, "MYSTR",
+                      PS_DATA_STRING,
+                      "String Item",
+                      extname);
+
+        // set the pixels in the image
+        psImageInit(image, (float)lcv);
+        if (!psFitsWriteImage(fitsFile, header, image, 0, extname)) {
+            diag("Could not write image");
+        }
+        psFree(header);
+    }
+    psFree(image);
+    psFree(fitsFile);
+
+    return true;
+}
+
+
+bool createFitsFile2(void)
+{
+    psFits* fitsFile = psFitsOpen(fitsFilename2, "w");
+
+    if (fitsFile == NULL) {
+        diag("Could not create 'multi' FITS file");
+        return false;
+    }
+
+    psImage* image = psImageAlloc(16, 16, PS_TYPE_F32);
+
+    char extname[80];
+    for (int lcv = 0; lcv < 8; lcv++) {
+        snprintf(extname, 80, "ext-%d", lcv);
+        pmHDU *hdu = pmHDUAlloc(extname);
+        hdu->header = psMetadataAlloc();
+
+        psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYINT",
+                      PS_DATA_S32,
+                      "psS32 Item", (psS32)lcv);
+
+        psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYFLT",
+                      PS_DATA_F32,
+                      "psF32 Item", (float)(1.0f/(float)(1+lcv)));
+
+        psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYDBL",
+                      PS_DATA_F64,
+                      "psF64 Item", (double)(1.0/(double)(1+lcv)));
+
+        psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYBOOL",
+                      PS_DATA_BOOL,
+                      "psBool Item",
+                      (lcv%2 == 0));
+
+        psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYSTR",
+                      PS_DATA_STRING,
+                      "String Item",
+                      extname);
+
+        // set the pixels in the image
+        psImageInit(image, (float)lcv);
+        if(!pmHDUWrite(hdu, fitsFile)) {
+            diag("Could not write header");
+            psErrorStackPrint(stdout, "THIS");
+
+exit(0);
+        }
+        if (!psFitsWriteImage(fitsFile, NULL, image, 0, extname)) {
+            diag("Could not write image");
+        }
+
+        psFree(hdu);
+    }
+    psFree(image);
+    psFree(fitsFile);
+
+    return true;
+}
+
+
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    plan_tests(18);
+      psError(PS_ERR_IO, true, "HEY: ERROR");
+
+    psHistogram *myHist = psHistogramAlloc(10, 5, 1);
+    psFree(myHist);
+
+    // Test pmHDUAlloc()
+    // Use a NULL extname
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUAlloc(NULL);
+        ok(hdu, "pmHDUAlloc(NULL) returned non-NULL");
+        skip_start(!hdu, 7, "Skipping tests because pmHDUAlloc(NULL) returned NULL");
+        ok(hdu->blankPHU == true, "pmHDUAlloc(NULL) set hdu->blankPHU correctly");
+        ok(hdu->extname == NULL, "pmHDUAlloc(NULL) set hdu->extname correctly");
+        ok(hdu->format == NULL, "pmHDUAlloc(NULL) set hdu->format correctly");
+        ok(hdu->header == NULL, "pmHDUAlloc(NULL) set hdu->header correctly");
+        ok(hdu->images == NULL, "pmHDUAlloc(NULL) set hdu->images correctly");
+        ok(hdu->weights == NULL, "pmHDUAlloc(NULL) set hdu->weights correctly");
+        ok(hdu->masks == NULL, "pmHDUAlloc(NULL) set hdu->masks correctly");
+        psFree(hdu);
+        skip_end();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Use a non-NULL extname
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUAlloc("ext-0");
+        ok(hdu, "pmHDUAlloc(extname) returned non-NULL");
+        skip_start(!hdu, 7, "Skipping tests because pmHDUAlloc(extname) returned NULL");
+        ok(hdu->blankPHU == false, "pmHDUAlloc(extname) set hdu->blankPHU correctly");
+        ok(0 == strcmp(hdu->extname, "ext-0"), "pmHDUAlloc(extname) set hdu->extname correctly");
+        ok(hdu->format == NULL, "pmHDUAlloc(extname) set hdu->format correctly");
+        ok(hdu->header == NULL, "pmHDUAlloc(extname) set hdu->header correctly");
+        ok(hdu->images == NULL, "pmHDUAlloc(extname) set hdu->images correctly");
+        ok(hdu->weights == NULL, "pmHDUAlloc(extname) set hdu->weights correctly");
+        ok(hdu->masks == NULL, "pmHDUAlloc(extname) set hdu->masks correctly");
+        psFree(hdu);
+        skip_end();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // tst_psFitsReadHeader()
+    {
+        ok(createFitsFile2(), "Created test FITS file");
+        psFits* fits = psFitsOpen(fitsFilename2,"r");
+        ok(fits != NULL, "psFitsOpen returned non-NULL on existing file");
+        int numHDUs = psFitsGetSize(fits);
+        ok(numHDUs == 8, "The test FITS file has %d HDUs", numHDUs);
+
+        for (int hdunum = 0; hdunum < numHDUs; hdunum++)
+        {
+            psMemId id = psMemGetId();
+            char extname[80];
+            snprintf(extname,80,"ext-%d",hdunum);
+            psFitsMoveExtNum(fits, hdunum, false);
+            pmHDU *hdu = pmHDUAlloc(extname);
+            bool rc = pmHDUReadHeader(hdu, fits);
+            ok(rc == true, "pmHDUReadHeader() returned TRUE");
+            ok(hdu->header != NULL &&
+               psMemCheckMetadata(hdu->header), "pmHDUReadHeader() correctly returned the hdu->header member");
+
+            // check for the extra metadata items
+            psS32 intItem = psMetadataLookupS32(NULL, hdu->header, "MYINT");
+            psF32 fltItem = psMetadataLookupF32(NULL, hdu->header, "MYFLT");
+            psF64 dblItem = psMetadataLookupF64(NULL, hdu->header, "MYDBL");
+            psMetadataItem* boolItem = psMetadataLookup(hdu->header, "MYBOOL");
+            psString strItem = psMetadataLookupStr(NULL, hdu->header, "MYSTR");
+
+            ok(intItem == hdunum, "Retrieved psS32 metadata item from file");
+            ok(fabsf(fltItem - 1.0f/(float)(1+hdunum)) <= FLT_EPSILON,
+               "Retrieved psF32 metadata item from file.  Got %f vs %f",
+               fltItem,1.0f/(float)(1+hdunum));
+            ok(abs(dblItem - 1.0/(double)(1+hdunum)) <= DBL_EPSILON,
+               "Retrieved psF64 metadata item from file.  Got %g vs %g",
+               dblItem, 1.0/(double)(1+hdunum));
+            ok(boolItem != NULL && boolItem->type == PS_DATA_BOOL,
+               "Retrieved psBool metadata item from file");
+            ok(strItem != NULL && strncmp(strItem,extname,strlen(extname)) == 0,
+               "Retrieved string metadata item from file.  Got '%s' vs '%s' (%d)",
+               strItem,extname,strlen(extname));
+            psFree(hdu);
+            ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks (header %d)", hdunum);
+        }
+
+
+        // Call pmHDUReadHeader() with NULL pmHDU input.  Should returne false.
+        {
+            psMemId id = psMemGetId();
+//            pmHDU *hdu = pmHDUAlloc(NULL);
+            bool rc = pmHDUReadHeader(NULL, fits);
+            ok(rc == false, "pmHDUReadHeader() returned FALSE with NULL hdu input");
+            ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        }
+
+        // Call pmHDUReadHeader() with NULL pmFits input.  Should returne false.
+        {
+            psMemId id = psMemGetId();
+            pmHDU *hdu = pmHDUAlloc(NULL);
+            bool rc = pmHDUReadHeader(hdu, NULL);
+            ok(rc == false, "pmHDUReadHeader() returned FALSE with NULL pmFits input");
+            psFree(hdu);
+            ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        }
+
+        psFree(fits);
+    }
+
+
+/* HERE
+    // tst_psFitsWriteHeader()
+    {
+        psMemId id = psMemGetId();
+        ok(createFitsFile2(), "Created 'multi' FITS file");
+
+        psMetadata* header   = psMetadataAlloc();
+        psFits*     fitsFile = psFitsOpen(fitsFilename,"a+");
+
+        // Test psFitsReadWrite generates files from psFitsWriteImage which 
+        // calls psFitsWriteHeader so these additional tests check for error conditions
+        // Attempt call function with NULL metadata
+        ok(!psFitsWriteHeader(fitsFile, NULL), "Expected return of true for NULL metadata pointer");
+        psFree(fitsFile);
+
+        // Attempt to call function with NULL fits
+        ok(!psFitsWriteHeader(NULL, header), "Expected return of true for NULL fits file pointer");
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+HERE*/
+}
Index: /branches/eam_branch_20071023/psModules/test/astrom/tst_pmAstrometry.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/astrom/tst_pmAstrometry.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/astrom/tst_pmAstrometry.c	(revision 15358)
@@ -0,0 +1,615 @@
+/** @file  tst_pmAstrometry.c
+ *
+ *  @brief Contains the tests: pmAstrometry.[ch].  The pmxxxAlloc()
+ *  and psFree() functionality are used here.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  XXX: Untested: pmFPACheckParents()
+ *  XXX: Create the pmHDU alloc/free function, test them here
+ *
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-01-26 21:10:51 $
+ *
+ *  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#include "config.h"
+#include <math.h>
+#include <string.h>
+#include "psTest.h"
+#include "pslib_strict.h"
+#include "pmAstrometry.h"
+
+static psS32 testFPAAlloc(void);
+static psS32 testChipAlloc(void);
+static psS32 testCellAlloc(void);
+static psS32 testReadoutAlloc(void);
+
+testDescription tests[] = {
+                              {testFPAAlloc,739,"pmFPAAlloc",0,false},
+                              {testChipAlloc,740,"pmChipAlloc",0,false},
+                              {testCellAlloc,741,"pmCellAlloc",0,false},
+                              {testReadoutAlloc,742,"pmReadoutAlloc",0,false},
+                              {NULL}
+                          };
+
+#define CHIP_ALLOC_NAME "ChipName"
+#define CELL_ALLOC_NAME "CellName"
+#define MISC_NUM 32
+#define MISC_NAME "META00"
+#define MISC_NAME2 "META01"
+#define NUM_BIAS_DATA 10
+#define TEST_NUM_ROWS 32
+#define TEST_NUM_COLS 32
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmFPA *generateSimpleFPA()
+{
+    psBool rc;
+    pmFPA* fpa = pmFPAAlloc(psMetadataAlloc());
+
+    if (fpa == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc() returned a NULL.");
+        return(NULL);
+    }
+
+    //
+    // Test and create camera metadata.
+    //
+    if (fpa->camera == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: fpa->camera is NULL.");
+        psFree(fpa);
+        return(NULL);
+    } else {
+        rc = psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+        if (rc == false) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not add metadata to fpa->camera.");
+            psFree(fpa);
+            return(NULL);
+        }
+        psS32 tmpS32 = psMetadataLookupS32(&rc, fpa->camera, MISC_NAME);
+        if ((rc == false) || (tmpS32 != MISC_NUM)) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not read metadata from fpa->camera.");
+            psFree(fpa);
+            return(NULL);
+        }
+    }
+
+    //
+    // Create various transforms and projections.
+    //
+    fpa->fromTangentPlane = PS_CREATE_4D_IDENTITY_PLANE_DISTORT();
+    fpa->toTangentPlane = PS_CREATE_4D_IDENTITY_PLANE_DISTORT();
+    fpa->projection = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+
+    //
+    // Ensure fpa concepts metadata was allocated properly.
+    //
+    if (fpa->concepts == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set fpa->concepts.");
+        psFree(fpa);
+        return(NULL);
+    } else {
+        rc = psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+        if (rc == false) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not add data to fpa->concepts.");
+            psFree(fpa);
+            return(NULL);
+        }
+        psS32 tmpS32 = psMetadataLookupS32(&rc, fpa->concepts, MISC_NAME);
+        if ((rc == false) || (tmpS32 != MISC_NUM)) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not read metadata from fpa->concepts.");
+            psFree(fpa);
+            return(NULL);
+        }
+    }
+
+    //
+    // Create ->analysis metadata.
+    //
+    fpa->analysis = psMetadataAlloc();
+    rc = psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+
+    //
+    // We test the fpa->chips array later.
+    //
+
+    //
+    // How to test the p_pmHDU *hdu member?
+    //
+
+    //
+    // Create ->phu metadata.
+    //
+    fpa->phu = psMetadataAlloc();
+    rc = psMetadataAddS32(fpa->phu, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+
+    return(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetLevel(PS_LOG_INFO);
+    psLogSetFormat("HLNM");
+
+    return ! runTestSuite(stderr,"pmAstrometry",tests,argc,argv);
+}
+
+/******************************************************************************
+testFPAAlloc()
+    1: We ensure that pmFPAAlloc() properly allocates a pmFPA struct.
+    2: We populate the members with real data to ensure they are being
+       free'ed correctly.
+ *****************************************************************************/
+static psS32 testFPAAlloc(void)
+{
+    psMetadata *camera = psMetadataAlloc();
+    pmFPA* fpa = pmFPAAlloc(camera);
+
+    if (fpa == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc() returned a NULL.");
+        return 1;
+    }
+
+    if (fpa->fromTangentPlane != NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->fromTangentPlane to NULL.");
+        return 2;
+    }
+
+    if (fpa->toTangentPlane != NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->toTangentPlane to NULL.");
+        return 3;
+    }
+    if (fpa->projection != NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->projection to NULL.");
+        return 4;
+    }
+
+    if (fpa->concepts == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->concepts.");
+        return 5;
+    }
+
+    if (fpa->analysis != NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->analysis to NULL.");
+        return 6;
+    }
+
+    if (fpa->camera != camera) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->camera.");
+        return 7;
+    }
+
+    if (fpa->chips == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->chips.");
+        return 8;
+    }
+
+    if (fpa->hdu != NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->hdu to NULL.");
+        return 9;
+    }
+    psFree(fpa);
+
+    //
+    // Populate the pmFPA struct with real data to ensure they were
+    // psFree()'ed correctly.
+    //
+    fpa = generateSimpleFPA();
+    if (fpa == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: generateSimpleFPA() returned NULL.");
+        return(15);
+    }
+    psFree(fpa);
+
+    return(0);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    psBool rc;
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    if (chip == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmChipAlloc returned a NULL.");
+        return(NULL);
+    }
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    //
+    // We already ensured that chip->concepts was working properly.
+    //
+
+    //
+    // Create ->analysis metadata.
+    //
+    chip->analysis = psMetadataAlloc();
+    rc = psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+
+    //
+    // We test the chip->cells array later.
+    //
+
+    //
+    // How to test the p_pmHDU *hdu member?
+    //
+
+    return(chip);
+}
+
+static psS32 testChipAlloc(void)
+{
+    pmFPA* fpa = generateSimpleFPA();
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    if (chip == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmChipAlloc returned a NULL.");
+        return 1;
+    }
+
+    if (chip->col0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->col0 set improperly.\n");
+        return 5;
+    }
+
+    if (chip->row0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->row0 set improperly.\n");
+        return 6;
+    }
+
+    if (chip->toFPA != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->toChip set improperly.\n");
+        return 7;
+    }
+
+    if (chip->fromFPA != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->toFPA set improperly.\n");
+        return 8;
+    }
+
+    if (chip->concepts == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->concepts set improperly.\n");
+        return 21;
+    } else {
+        psMetadataItem *tmpMeta = psMetadataLookup(chip->concepts, "CHIP.NAME");
+        if (0 != strcmp((char *) tmpMeta->data.V, CHIP_ALLOC_NAME)) {
+            psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: The metadata was set improperly.\n");
+            return (32);
+        }
+        // XXX: Code a test to ensure the metadata has the correct type
+    }
+
+    if (chip->analysis != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->analysis set improperly.\n");
+        return 10;
+    }
+
+    if (chip->cells == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->cells set improperly.\n");
+        return 22;
+    }
+
+    if (chip->parent != fpa) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->parent set improperly.\n");
+        return 23;
+    }
+
+    if (chip->valid != false) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->valid set improperly.\n");
+        return 24;
+    }
+
+    if (chip->hdu != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->hdu set improperly.\n");
+        return 25;
+    }
+    psFree(fpa);
+
+    //
+    // Populate the pmChip struct with real data to ensure they were
+    // psFree()'ed correctly.
+    //
+    fpa = generateSimpleFPA();
+    chip = generateSimpleChip(fpa);
+    psFree(fpa);
+
+    return(0);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmFPA *fpa, pmChip *chip)
+{
+    psBool rc;
+    pmCell *cell = pmCellAlloc(chip, (psMetadata *) fpa->camera, CELL_ALLOC_NAME);
+    if (cell == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmCellAlloc returned a NULL.");
+        return(NULL);
+    }
+    cell->toChip = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    cell->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    cell->toSky = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    //
+    // We already ensured that cell->concepts was working properly.
+    //
+
+    //
+    // Test camera metadata.
+    //
+    if (cell->camera == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->camera is NULL.");
+        psFree(fpa);
+        return(NULL);
+    } else {
+        rc = psMetadataAddS32((psMetadata *) cell->camera, PS_LIST_HEAD, MISC_NAME2, 0, NULL, MISC_NUM);
+        if (rc == false) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not add metadata to cell->camera.");
+            psFree(fpa);
+            return(NULL);
+        }
+        psS32 tmpS32 = psMetadataLookupS32(&rc, cell->camera, MISC_NAME2);
+        if ((rc == false) || (tmpS32 != MISC_NUM)) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not read metadata from cell->camera.");
+            psFree(fpa);
+            return(NULL);
+        }
+    }
+
+    //
+    // Create ->analysis metadata.
+    //
+    cell->analysis = psMetadataAlloc();
+    rc = psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+
+    //
+    // We test the cell->readouts array later.
+    //
+
+    //
+    // How to test the p_pmHDU *hdu member?
+    //
+
+    return(cell);
+}
+
+static psS32 testCellAlloc(void)
+{
+    pmFPA* fpa = generateSimpleFPA();
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    pmCell *cell = pmCellAlloc(chip, (psMetadata *) fpa->camera, CELL_ALLOC_NAME);
+    if (cell == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmCellAlloc returned a NULL.n");
+        return 3;
+    }
+
+    if (cell->col0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->col0 set improperly.\n");
+        return 5;
+    }
+
+    if (cell->row0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->row0 set improperly.\n");
+        return 6;
+    }
+
+    if (cell->toChip != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->toChip set improperly.\n");
+        return 7;
+    }
+
+    if (cell->toFPA != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->toFPA set improperly.\n");
+        return 8;
+    }
+
+    if (cell->toSky != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->toSky set improperly.\n");
+        return 9;
+    }
+
+    if (cell->concepts == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->concepts set improperly.\n");
+        return 21;
+    } else {
+        psMetadataItem *tmpMeta = psMetadataLookup(cell->concepts, "CELL.NAME");
+        if (0 != strcmp((char *) tmpMeta->data.V, CELL_ALLOC_NAME)) {
+            psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: The metadata was set improperly.\n");
+            return (32);
+        }
+        // XXX: Code a test to ensure the metadata has the correct type
+    }
+
+    if (cell->camera != fpa->camera) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->camera set improperly.\n");
+        return 20;
+    }
+
+    if (cell->analysis != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->analysis set improperly.\n");
+        return 10;
+    }
+
+    if (cell->readouts == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->readouts set improperly.\n");
+        return 22;
+    }
+
+    if (cell->parent != chip) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->parent set improperly.\n");
+        return 23;
+    }
+
+    if (cell->valid != false) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->valid set improperly.\n");
+        return 24;
+    }
+
+    if (cell->hdu != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->hdu set improperly.\n");
+        return 27;
+    }
+    psFree(fpa);
+
+    //
+    // Populate the pmCell struct with real data to ensure they were
+    // psFree()'ed correctly.
+    //
+    fpa = generateSimpleFPA();
+    chip = generateSimpleChip(fpa);
+    cell = generateSimpleCell(fpa, chip);
+    psFree(fpa);
+
+    return(0);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    psBool rc;
+    pmReadout *readout = pmReadoutAlloc(cell);
+    if (readout == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmReadoutAlloc returned a NULL.");
+        return(NULL);
+    }
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+
+    //
+    // Create a psList of bias data.
+    //
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        if (readout->bias == NULL) {
+            readout->bias = psListAlloc(tmpImage);
+        } else {
+            psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        }
+    }
+
+    //
+    // Test readout->analysis metadata.
+    //
+    if (readout->analysis == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: readout->analysis is NULL.");
+        psFree(fpa);
+        return(NULL);
+    } else {
+        rc = psMetadataAddS32((psMetadata *) readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+        if (rc == false) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not add metadata to readout->analysis.");
+            psFree(fpa);
+            return(NULL);
+        }
+    }
+
+    return(readout);
+}
+
+
+static psS32 testReadoutAlloc(void)
+{
+    pmFPA* fpa = generateSimpleFPA();
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    pmCell *cell = pmCellAlloc(chip, (psMetadata *) fpa->camera, CELL_ALLOC_NAME);
+    pmReadout *readout = pmReadoutAlloc(cell);
+    if (readout == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmReadoutAlloc returned a NULL.\n");
+        return 4;
+    }
+
+    if (readout->col0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->col0 set improperly.\n");
+        return 5;
+    }
+
+    if (readout->row0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->row0 set improperly.\n");
+        return 6;
+    }
+
+    if (readout->colBins != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->colBins set improperly.\n");
+        return 7;
+    }
+
+    if (readout->rowBins != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->colBins set improperly.\n");
+        return 8;
+    }
+
+    if (readout->image != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->image set improperly.\n");
+        return 10;
+    }
+
+    if (readout->mask != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->mask set improperly.\n");
+        return 12;
+    }
+
+    if (readout->weight != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->weight set improperly.\n");
+        return 14;
+    }
+
+    if (readout->bias != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->bias set improperly.\n");
+        return 16;
+    }
+
+    if (readout->analysis == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->analysis set improperly.\n");
+        return 18;
+    }
+
+    if (readout->parent != cell) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->parent set improperly.\n");
+        return 20;
+    }
+    psFree(fpa);
+
+    //
+    // Populate the pmReadout struct with real data to ensure they were
+    // psFree()'ed correctly.
+    //
+    fpa = generateSimpleFPA();
+    chip = generateSimpleChip(fpa);
+    cell = generateSimpleCell(fpa, chip);
+    readout = generateSimpleReadout(fpa, chip, cell);
+    psFree(fpa);
+
+    return(0);
+}
Index: /branches/eam_branch_20071023/psModules/test/astrom/tst_pmAstrometry01.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/astrom/tst_pmAstrometry01.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/astrom/tst_pmAstrometry01.c	(revision 15358)
@@ -0,0 +1,675 @@
+/** @file  tst_psAstrometry01.c
+*
+*  @brief This code will test the pmFPA hierarchy transform code in psAstrometry.[ch]
+*
+*  @author GLG, MHPCC
+*
+*  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2006-03-06 22:53:47 $
+*
+* XXX: Add tests were the coordinate does not transform to any legitimate cell
+* or chip, or FPA, or whatever.
+*
+* XXX: For each function, add tests for bad input parameters, as well as failed transforms.
+*
+* XXX: Must test pmFPASelectChip() and pmFPAExcludeChip().
+*
+*  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+*/
+#include "config.h"
+#include <math.h>
+#include <string.h>
+#include "psTest.h"
+#include "pslib_strict.h"
+#include "pmAstrometry.h"
+static psS32 test3( void );
+static psS32 test4( void );
+static psS32 test5( void );
+
+testDescription tests[] = {
+                              {test3, 666, "pmAstrometry focal plane transformations", 0, false},
+                              {test4, 667, "pmCheckParents()", 0, false},
+                              {test5, 668, "pmFPASelectChip() and pmFPAExcludeChip()", 0, false},
+                              {NULL}
+                          };
+
+psS32 currentId = 0;
+psS32 memLeaks = 0;
+psS32 main( psS32 argc, char* argv[] )
+{
+    psLogSetLevel( PS_LOG_INFO );
+    psLogSetFormat("HLNM");
+
+    return ( ! runTestSuite( stderr, "psAstrometry", tests, argc, argv ) );
+}
+
+#define PS_PERCENT_COMPARE(X, Y, PERCENT_FRACTION) (fabs((Y)-(X))/fabs(X) < (PERCENT_FRACTION))
+#define VERBOSE 0
+#define NUM_READOUTS 1
+#define READOUT_NUM_ROWS 10
+#define READOUT_NUM_COLS 10
+
+#define NUM_CELLS 4
+#define CELL_GAP 2
+#define CELL_WIDTH READOUT_NUM_COLS
+#define CELL_HEIGHT READOUT_NUM_ROWS
+#define CELL_MIN_X 0
+#define CELL_MAX_X CELL_HEIGHT
+#define CELL_MIN_Y 0
+#define CELL_MAX_Y CELL_WIDTH
+
+#define NUM_CHIPS 2
+#define CHIP_GAP 2
+#define CHIP_MIN_X 0
+#define CHIP_MAX_X CELL_HEIGHT
+#define CHIP_MIN_Y 0
+#define CHIP_MAX_Y ((NUM_CELLS * CELL_WIDTH) + ((NUM_CELLS) * CELL_GAP))
+#define CHIP_WIDTH CHIP_MAX_Y
+#define CHIP_HEIGHT CHIP_MAX_X
+
+#define NUM_FPAS 1
+#define FPA_MIN_X 0
+#define FPA_MAX_X CHIP_HEIGHT
+#define FPA_MIN_Y 0
+#define FPA_MAX_Y (((NUM_CHIPS) * CHIP_WIDTH) + (((NUM_CHIPS)-1) * CHIP_GAP))
+
+#define PROJECTION_SCALE_X 1.0
+#define PROJECTION_SCALE_Y 1.0
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+genSystem(): This routine will create a system of FPAs/Chips/Cells/Readouts.  For
+simplicity, an FPA is defined as a linear array of chips, and a chip is
+defined as a linear array of cells, both in the y (cols) direction.  The
+transforms between the various layers take into account the cell/chip and
+the boundaries between each.
+ *****************************************************************************/
+pmFPA *genSystem()
+{
+    //
+    // Create top pmFPA structure.
+    //
+    const psMetadata *camera = psMetadataAlloc();
+    pmFPA* myFPA = pmFPAAlloc(camera);
+    if (myFPA == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc() returned a NULL.\n");
+        return NULL;
+    }
+    myFPA->fromTangentPlane = PS_CREATE_4D_IDENTITY_PLANE_DISTORT();
+    myFPA->toTangentPlane = PS_CREATE_4D_IDENTITY_PLANE_DISTORT();
+    myFPA->projection = psProjectionAlloc(0.0,0.0,PROJECTION_SCALE_X,PROJECTION_SCALE_X,PS_PROJ_TAN);
+
+    myFPA->chips = psArrayRealloc(myFPA->chips, NUM_CHIPS);
+    for (psS32 chipID=0 ; chipID<NUM_CHIPS ; chipID++) {
+        pmChip *myChip = pmChipAlloc(myFPA, "ChipName");
+        if (myChip == NULL) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmChipAlloc() returned a NULL.\n");
+            return NULL;
+        }
+        myFPA->chips->data[chipID] = (psPtr *) myChip;
+        myChip->row0 = 0;
+        myChip->col0 = chipID * (CHIP_WIDTH + CHIP_GAP);
+
+        // We create the transforms between the chip and FPA.  The
+        // transform is a simple identity transform.
+        myChip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+        myChip->toFPA->y->coeff[0][0] = (psF64) myChip->col0;
+        myChip->toFPA->y->coeff[1][1] = 0.0;
+        myChip->toFPA->x->coeff[0][0] = (psF64) myChip->row0;
+        myChip->toFPA->x->coeff[1][1] = 0.0;
+
+        myChip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+        myChip->fromFPA->y->coeff[0][0] = (psF64) (- myChip->col0);
+        myChip->fromFPA->y->coeff[1][1] = 0.0;
+        myChip->fromFPA->x->coeff[0][0] = (psF64) (- myChip->row0);
+        myChip->fromFPA->x->coeff[1][1] = 0.0;
+
+        myChip->cells = psArrayRealloc(myChip->cells, NUM_CELLS);
+        for (psS32 cellID=0 ; cellID<NUM_CELLS ; cellID++) {
+            pmCell *myCell = pmCellAlloc(myChip, NULL, "CellName");
+            if (myCell == NULL) {
+                psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmCellAlloc() returned a NULL.\n");
+                return NULL;
+            }
+            myChip->cells->data[cellID] = (psPtr *) myCell;
+            myCell->row0 = 0;
+            myCell->col0 = cellID * (CELL_WIDTH + CELL_GAP);
+            myCell->toChip = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+            myCell->toChip->y->coeff[0][0] = (psF64) myCell->col0;
+            myCell->toChip->y->coeff[1][1] = 0.0;
+            myCell->toChip->x->coeff[0][0] = (psF64) myCell->row0;
+            myCell->toChip->x->coeff[1][1] = 0.0;
+
+            myCell->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+            myCell->toFPA->y->coeff[0][0] = (psF64) (myCell->col0 + myChip->col0);
+            myCell->toFPA->y->coeff[1][1] = 0.0;
+            myCell->toFPA->x->coeff[0][0] = (psF64) (myCell->row0 + myChip->row0);
+            myCell->toFPA->x->coeff[1][1] = 0.0;
+
+            myCell->toSky = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+            myCell->toSky->y->coeff[0][0] = myCell->toFPA->y->coeff[0][0];
+            myCell->toSky->y->coeff[1][1] = 0.0;
+            myCell->toSky->x->coeff[0][0] = myCell->toFPA->x->coeff[0][0];
+            myCell->toSky->x->coeff[1][1] = 0.0;
+
+            myCell->readouts = psArrayRealloc(myCell->readouts, NUM_READOUTS);
+            for (psS32 readoutID=0 ; readoutID<NUM_READOUTS ; readoutID++) {
+                pmReadout *myReadout = pmReadoutAlloc(myCell);
+                if (myReadout == NULL) {
+                    psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmReadoutAlloc() returned a NULL.\n");
+                    return NULL;
+                }
+                myCell->readouts->data[readoutID] = (psPtr *) myReadout;
+                myReadout->image = psImageAlloc(READOUT_NUM_COLS, READOUT_NUM_ROWS, PS_TYPE_F32);
+                for (psS32 row=0;row<READOUT_NUM_ROWS;row++) {
+                    for(psS32 col=0;col<READOUT_NUM_COLS;col++) {
+                        myReadout->image->data.F32[row][col] = (psF32) ((chipID * (CHIP_WIDTH + CHIP_GAP)) +
+                                                               (cellID * (CELL_WIDTH + CELL_GAP)));
+                    }
+                }
+                myReadout->row0 = myCell->row0;
+                myReadout->col0 = myCell->col0;
+                myReadout->rowBins = 0;
+                myReadout->colBins = 0;
+            }
+            if (VERBOSE) {
+                printf("\n\n\n\nFor chip %d cell %d the cell->toFPA transform is:\n", chipID, cellID);
+                PS_PRINT_PLANE_TRANSFORM(myCell->toFPA);
+                printf("\n\n\n\nFor chip %d cell %d the cell->toChip transform is:\n", chipID, cellID);
+                PS_PRINT_PLANE_TRANSFORM(myCell->toChip);
+                printf("\n\n\n\nFor chip %d cell %d the cell->toSky transform is:\n", chipID, cellID);
+                PS_PRINT_PLANE_TRANSFORM(myCell->toSky);
+            }
+        }
+        if (VERBOSE) {
+            printf("\n\n\n\nFor chip %d the chip->toFPA transform is:\n", chipID);
+            PS_PRINT_PLANE_TRANSFORM(myChip->toFPA);
+            printf("\n\n\n\nFor chip %d the chip->fromFPA transform is:\n", chipID);
+            PS_PRINT_PLANE_TRANSFORM(myChip->fromFPA);
+        }
+    }
+
+    return(myFPA);
+}
+
+
+/******************************************************************************
+This routine tests many Astrometry functions.  It loops through all valid
+pixels of all cells of all chips and computes the corresponding (x,y)
+coordinates in the FPA plane.  It then calls the pmChipInFPA() then
+pmCellInFPA() with the FPA coordinate and determines the chip/cell that that
+coordinate corresponds to.  Following that it does a variety of tests on the
+various functions that tharnsform coordinates within the pmFPA hierarchy.
+ 
+List of tested functions:
+    pmCellInFPA()  yes
+    pmChipInFPA()  yes
+    pmCellInChip()  yes
+ 
+    pmCoordCellToFPA()  yes
+    pmCoordChipToFPA()  yes
+    pmCoordFPAToChip()  yes
+    pmCoordCellToChip()  yes
+    pmCoordChipToCell()  yes
+ 
+    pmCoordFPAToTP()  yes
+    pmCoordTPToFPA()  yes
+ 
+    pmCoordTPToSky()  yes
+    pmCoordSkyToTP()  yes
+    pmCoordSkyToCell()  yes
+    pmCoordCellToSky()  yes
+    pmCoordCellToSkyQuick() yes
+    pmCoordSkyToCellQuick() yes
+ *****************************************************************************/
+psS32 test3( void )
+{
+    psS32 x;
+    psS32 y;
+    psPlane fpaCoord;
+    pmFPA *myFPA = genSystem();
+    pmCell *myCell = NULL;
+    psPlane chipCoord;
+    psPlane cellCoord;
+    psPlane testCoord;
+    psSphere *skyCoord = psSphereAlloc();
+    // XXX: This code causes a seg fault.
+    //    psSphere skyTmp;
+    //    psMemCheckType(PS_DATA_SPHERE, &skyTmp);
+    psPlane tpCoord;
+    psS32 testStatus = 0;
+
+    //
+    // I'm not convinced that the p_psProject() and p_psDeproject() functions work
+    // correctly.  If we project a set of coordinates over a wide range of (R, D)
+    // values, then deproject them, the original (R, D) values are only produced
+    // when D is larger than 0.  This code demonstrates that.  I also created tests
+    // that currently fail in tst_psCoord01.c.  I have a workaround in the function
+    // XXXDeproject() in pmAstrometry.c.
+    //
+    if (0) {
+        // This loop goes from (R, D) -> (X, Y) -> (R, D)
+        psPlane planeCoord01;
+        psSphere skyCoord01;
+        psSphere skyCoord02;
+        #define DEG_INC 15.0
+
+        for (psF32 R = -90.0 ; R <= 90.0 ; R+= DEG_INC) {
+            for (psF32 D = -90.0 ; D <= 90.0 ; D+= DEG_INC) {
+                if ((fabs(R) != 90.0) && (fabs(D) != 90.0)) {
+                    skyCoord01.r = DEG_TO_RAD(R);
+                    skyCoord01.d = DEG_TO_RAD(D);
+                    p_psProject(&planeCoord01, &skyCoord01, myFPA->projection);
+                    p_psDeproject(&skyCoord02, &planeCoord01, myFPA->projection);
+                    printf("(%.2fr %.2fd) (%.2fr %.2fd) -> (%.2f %.2f) -> (%.2fr %.2fd)", R, D,
+                           skyCoord01.r, skyCoord01.d,
+                           planeCoord01.x, planeCoord01.y,
+                           skyCoord02.r, skyCoord02.d);
+                    if ((fabs(skyCoord01.r - skyCoord02.r) < FLT_EPSILON) &&
+                            (fabs(skyCoord01.d - skyCoord02.d) < FLT_EPSILON)) {
+                        printf(": CORRECT\n");
+                    } else {
+                        printf(": WRONG\n");
+                    }
+                }
+            }
+        }
+        psFree(myFPA);
+        return(0);
+    }
+    if (0) {
+        // This loop goes from (X, Y) -> (R, D) -> (X, Y)
+        #define SPACE_INC 4.0
+        for(testCoord.x=-CELL_HEIGHT;testCoord.x<=CELL_HEIGHT;testCoord.x+=SPACE_INC)
+        {
+            for (testCoord.y=-CELL_WIDTH;testCoord.y<=CELL_WIDTH;testCoord.y+=SPACE_INC) {
+                psPlane planeCoord01;
+                psPlane planeCoord02;
+                psSphere skyCoord01;
+                psSphere skyCoord02;
+                p_psDeproject(&skyCoord01, &testCoord, myFPA->projection);
+                p_psProject(&planeCoord01, &skyCoord01, myFPA->projection);
+                p_psDeproject(&skyCoord02, &planeCoord01, myFPA->projection);
+                p_psProject(&planeCoord02, &skyCoord02, myFPA->projection);
+                printf("Plane: (%.2f %.2f) -> (%.2fr %.2fd) -> (%.2f %.2f)\n",
+                       testCoord.x, testCoord.y,
+                       skyCoord01.r, skyCoord01.d,
+                       planeCoord01.x, planeCoord01.y);
+                /*
+                                printf("Plane: (%.2f %.2f) -> (%.2f %.2f) -> (%.2f %.2f)\n",
+                                        testCoord.x, testCoord.y, planeCoord01.x, planeCoord01.y,
+                                        planeCoord02.x, planeCoord02.y);
+                                printf("Sphere: (%.2f %.2f) -> (%.2f %.2f)\n",
+                                        skyCoord01.r, skyCoord01.d, skyCoord02.r, skyCoord02.d);
+                                printf("Plane: (%.2f %.2f) -> (%.2fd %.2fr) -> (%.2f %.2f) -> (%.2fd %.2fr) -> (%.2f %.2f)\n",
+                                        testCoord.x, testCoord.y,
+                                        skyCoord01.r, skyCoord01.d,
+                                        planeCoord01.x, planeCoord01.y,
+                                        skyCoord02.r, skyCoord02.d,
+                                        planeCoord02.x, planeCoord02.y);
+                */
+            }
+        }
+        psFree(myFPA);
+        return(0);
+    }
+    //
+    // We iterate through all cells on all chips on the fpa.  We determine
+    // the expected fpaCcoord.
+    //
+
+    for (psS32 chip=0;chip<NUM_CHIPS;chip++) {
+        for (psS32 cell=0;cell<NUM_CELLS;cell++) {
+            for(x=0;x<CELL_HEIGHT;x++) {
+                for (y=0;y<CELL_WIDTH;y++) {
+                    fpaCoord.x = (psF64) x;
+                    fpaCoord.y = (psF64) (y + (chip * (CHIP_WIDTH + CHIP_GAP)) +
+                                          (cell * (CELL_WIDTH + CELL_GAP)));
+                    if (VERBOSE) {
+                        printf("------------------ (%.2f, %.2f) ------------------\n", fpaCoord.x, fpaCoord.y);
+                        printf("(chip, cell, x, y) is (%d, %d, %d, %d)\n", chip, cell, x, y);
+                    }
+                    pmChip* tmpChip = pmChipInFPA(&fpaCoord, myFPA);
+                    myCell = pmCellInFPA(&fpaCoord, myFPA);
+
+                    if ((myCell == NULL) || (tmpChip == NULL)) {
+                        if (tmpChip == NULL) {
+                            printf("TEST ERROR: pmChipInFPA() returned NULL\n");
+                            testStatus = 1;
+                        } else if (myCell == NULL) {
+                            printf("TEST ERROR: pmCellInFPA(): returned NULL\n");
+                            testStatus = 1;
+                        }
+                    } else {
+                        pmCoordFPAToChip(&chipCoord, &fpaCoord, tmpChip);
+                        pmCoordChipToCell(&cellCoord, &chipCoord, myCell);
+
+                        if (x != (psS32) cellCoord.x) {
+                            printf("TEST ERROR: pmCoordFPAToChip()->pmCoordChipToCell(): x coord was %d (%f), should be %d\n", (psS32) cellCoord.x, cellCoord.x, x);
+                            testStatus = 1;
+                        }
+                        if (y != (psS32) cellCoord.y) {
+                            printf("TEST ERROR: pmCoordFPAToChip()->pmCoordChipToCell(): y coord was %d (%f), should be %d\n", (psS32) cellCoord.y, cellCoord.y, y);
+                            testStatus = 1;
+                        }
+
+                        pmCoordCellToChip(&testCoord, &cellCoord, myCell);
+                        if (fabs(testCoord.x - chipCoord.x) > FLT_EPSILON) {
+                            printf("TEST ERROR: pmCoordCellToChip() x coord was %.2f, should be %d\n", cellCoord.x, x);
+                            testStatus = 1;
+                        }
+                        if (fabs(testCoord.y - chipCoord.y) > FLT_EPSILON) {
+                            printf("TEST ERROR: pmCoordCellToChip() y coord was %.2f, should be %d\n", cellCoord.y, y);
+                            testStatus = 1;
+                        }
+
+                        pmCell *myCell2 = pmCellInChip(&chipCoord, tmpChip);
+                        if (myCell2 != myCell) {
+                            printf("TEST ERROR: pmCellInChip() != pmCellInChip(pmChipInFPA()) (%p %p)\n", myCell2, myCell);
+                            testStatus = 1;
+                        }
+
+                        pmCoordChipToFPA(&testCoord, &chipCoord, tmpChip);
+                        if (fabs(testCoord.x - x) > FLT_EPSILON) {
+                            printf("TEST ERROR: pmCoordChipToFPA() x coord was %.2f, should be %d\n", cellCoord.x, x);
+                            testStatus = 1;
+                        }
+                        if (fabs(testCoord.y - fpaCoord.y) > FLT_EPSILON) {
+                            printf("TEST ERROR: pmCoordChipToFPA() y coord was %.2f, should be %.2f\n", cellCoord.y, fpaCoord.y);
+                            testStatus = 1;
+                        }
+
+                        pmCoordFPAToTP(&testCoord, &fpaCoord, 0.0, 0.0, myFPA);
+                        if (fabs(testCoord.x - fpaCoord.x) > FLT_EPSILON) {
+                            printf("TEST ERROR: pmCoordFPAToTP() x coord was %.2f, should be %d\n", cellCoord.x, x);
+                            testStatus = 1;
+                        }
+                        if (fabs(testCoord.y - fpaCoord.y) > FLT_EPSILON) {
+                            printf("TEST ERROR: pmCoordFPAToTP() y coord was %.2f, should be %d\n", cellCoord.y, y);
+                            testStatus = 1;
+                        }
+
+                        //
+                        // Test pmCoordTPToSky() -> pmCoordSkyToTP()
+                        //
+                        if (1) {
+                            psSphere *rc = pmCoordTPToSky(skyCoord, &testCoord, myFPA->projection);
+                            if (rc == NULL) {
+                                printf("pmCoordTPToSky() failed.\n");
+                            } else {
+                                psPlane *rc = pmCoordSkyToTP(&tpCoord, skyCoord, myFPA->projection);
+                                if (rc == NULL) {
+                                    printf("pmCoordSkyToTP() failed.\n");
+                                } else {
+                                    if (fabs(testCoord.x - tpCoord.x) > FLT_EPSILON) {
+                                        printf("TEST ERROR: pmCoordTPToSky()/pmCoordSkyToTP() x coord was %.2f, should be %.2f\n", tpCoord.x, testCoord.x);
+                                        testStatus = 1;
+                                    }
+                                    if (fabs(testCoord.y - tpCoord.y) > FLT_EPSILON) {
+                                        printf("TEST ERROR: pmCoordTPToSky()/pmCoordSkyToTP() y coord was %.2f, should be %.2f\n", tpCoord.y, testCoord.y);
+                                        testStatus = 1;
+                                    }
+                                    if (VERBOSE) {
+                                        printf("(%.2f %.2f) -> (%.2f %.2f) -> (%.2f %.2f)\n", testCoord.x, testCoord.y, skyCoord->r, skyCoord->d, tpCoord.x, tpCoord.y);
+                                    }
+                                }
+                            }
+                        }
+
+                        //
+                        // Test pmCoordCellToSky() -> pmCoordSkyToCell()
+                        //
+                        if (1) {
+                            psPlane tmpCellCoord;
+                            psSphere *rc = pmCoordCellToSky(skyCoord, &cellCoord, 0.0, 0.0, myCell);
+                            if (rc == NULL) {
+                                printf("pmCoordCellToSky() failed.\n");
+                            } else {
+                                psPlane *rc = pmCoordSkyToCell(&tmpCellCoord, skyCoord, 0.0, 0.0, myCell);
+                                if (rc == NULL) {
+                                    printf("pmCoordSkyToCell() failed.\n");
+                                } else {
+                                    if (fabs(cellCoord.x - tmpCellCoord.x) > FLT_EPSILON) {
+                                        printf("TEST ERROR: pmCoordCellToSky()/pmCoordSkyToCell() x coord was %.2f, should be %.2f\n", tmpCellCoord.x, cellCoord.x);
+                                        testStatus = 1;
+                                    }
+                                    if (fabs(cellCoord.y - tmpCellCoord.y) > FLT_EPSILON) {
+                                        printf("TEST ERROR: pmCoordCellToSky()/pmCoordSkyToCell() y coord was %.2f, should be %.2f\n", tmpCellCoord.y, cellCoord.y);
+                                        testStatus = 1;
+                                    }
+                                    if (VERBOSE) {
+                                        printf("(%.2f %.2f) -> (%.2f %.2f) -> (%.2f %.2f)\n", testCoord.x, testCoord.y, skyCoord->r, skyCoord->d, tmpCellCoord.x, tmpCellCoord.y);
+                                    }
+                                }
+                            }
+                        }
+
+                        //
+                        // Test pmCoordCellToSkyQuick() -> pmCoordSkyToCellQuick()
+                        // I'm not sure how to test this in a system with chip and cell gaps.
+                        // There's no way to create an accurate polynomial transform from
+                        // a cell to the sky where there are cell, or chip gaps.
+                        //
+                        if ((NUM_CHIPS == 1) && (NUM_CELLS == 1)) {
+                            psPlane tmpCellCoord;
+                            psSphere *rc = pmCoordCellToSkyQuick(skyCoord, &cellCoord, myCell);
+                            if (rc == NULL) {
+                                printf("pmCoordCellToSkyQuick() failed.\n");
+                            } else {
+                                psPlane *rc = pmCoordSkyToCellQuick(&tmpCellCoord, skyCoord, myCell);
+                                if (rc == NULL) {
+                                    printf("pmCoordSkyToCellQuick() failed.\n");
+                                } else {
+                                    if (fabs(cellCoord.x - tmpCellCoord.x) > FLT_EPSILON) {
+                                        printf("TEST ERROR: pmCoordCellToSky()/pmCoordSkyToCell() x coord was %.2f, should be %.2f\n", tmpCellCoord.x, cellCoord.x);
+                                        testStatus = 1;
+                                    }
+                                    if (fabs(cellCoord.y - tmpCellCoord.y) > FLT_EPSILON) {
+                                        printf("TEST ERROR: pmCoordCellToSky()/pmCoordSkyToCell() y coord was %.2f, should be %.2f\n", tmpCellCoord.y, cellCoord.y);
+                                        testStatus = 1;
+                                    }
+                                    if (VERBOSE) {
+                                        printf("(%.2f %.2f) -> (%.2f %.2f) -> (%.2f %.2f)\n", testCoord.x, testCoord.y, skyCoord->r, skyCoord->d, tmpCellCoord.x, tmpCellCoord.y);
+                                    }
+                                }
+                            }
+                        }
+
+                        pmCoordCellToFPA(&testCoord, &cellCoord, myCell);
+                        if (fabs(testCoord.x - fpaCoord.x) > FLT_EPSILON) {
+                            printf("TEST ERROR: pmCoordCellToFPA() x coord was %.2f, should be %d\n", cellCoord.x, x);
+                            testStatus = 1;
+                        }
+                        if (fabs(testCoord.y - fpaCoord.y) > FLT_EPSILON) {
+                            printf("TEST ERROR: pmCoordCellToFPA() y coord was %.2f, should be %d\n", cellCoord.y, y);
+                            testStatus = 1;
+                        }
+
+                        pmCoordTPToFPA(&testCoord, &fpaCoord, 0.0, 0.0, myFPA);
+                        if (fabs(testCoord.x - fpaCoord.x) > FLT_EPSILON) {
+                            printf("TEST ERROR: pmCoordTPToFPA() x coord was %.2f, should be %d\n", cellCoord.x, x);
+                            testStatus = 1;
+                        }
+                        if (fabs(testCoord.y - fpaCoord.y) > FLT_EPSILON) {
+                            printf("TEST ERROR: pmCoordTPToFPA() y coord was %.2f, should be %d\n", cellCoord.y, y);
+                            testStatus = 1;
+                        }
+                    }
+                }
+            }
+        }
+    }
+    psFree(myFPA);
+    psFree(skyCoord);
+
+    return(testStatus);
+}
+
+/******************************************************************************
+test4(): This routine wil test the pmFPACheckParents() function.  We generate
+an pmFPA hierarchy, then set the parents of each readout/cell/chip to NULL,
+then call pmFPACheckParents() to restore them, then we ensure they were
+restored.
+ *****************************************************************************/
+psS32 test4( void )
+{
+    psS32 testStatus = 0;
+
+    //
+    // Generate a pmFPA hierarchy.
+    //
+    pmFPA *tmpFPA = genSystem();
+
+    //
+    // We set the parents of each readout/cell/chip to NULL.
+    //
+    for (psS32 chipID = 0; chipID < tmpFPA->chips->n ; chipID++) {
+        pmChip *tmpChip = (pmChip *) tmpFPA->chips->data[chipID];
+        tmpChip->parent = NULL;
+
+        for (psS32 cellID = 0; cellID < tmpChip->cells->n ; cellID++) {
+            pmCell *tmpCell = (pmCell *) tmpChip->cells->data[cellID];
+            tmpCell->parent = NULL;
+
+            for (psS32 readoutID = 0; readoutID < tmpCell->readouts->n ; readoutID++) {
+                pmReadout *tmpReadout = (pmReadout *) tmpCell->readouts->data[readoutID];
+                tmpReadout->parent = NULL;
+            }
+        }
+    }
+
+    //
+    // Ensure that pmFPACheckParents() returned FALSE.
+    //
+    psBool rc = pmFPACheckParents(tmpFPA);
+    if (rc != false) {
+        printf("TEST ERROR: pmCheckParents() returned TRUE.\n");
+        testStatus = 1;
+    }
+
+    //
+    // Ensure that the parent members are right.
+    //
+    for (psS32 chipID = 0; chipID < tmpFPA->chips->n ; chipID++) {
+        pmChip *tmpChip = (pmChip *) tmpFPA->chips->data[chipID];
+        if (tmpChip->parent != tmpFPA) {
+            printf("TEST ERROR: pmCheckParents() did not restore Chip->parent.\n");
+            testStatus = 2;
+        }
+
+        for (psS32 cellID = 0; cellID < tmpChip->cells->n ; cellID++) {
+            pmCell *tmpCell = (pmCell *) tmpChip->cells->data[cellID];
+            if (tmpCell->parent != tmpChip) {
+                printf("TEST ERROR: pmCheckParents() did not restore Cell->parent.\n");
+                testStatus = 3;
+            }
+
+            for (psS32 readoutID = 0; readoutID < tmpCell->readouts->n ; readoutID++) {
+                pmReadout *tmpReadout = (pmReadout *) tmpCell->readouts->data[readoutID];
+                if (tmpReadout->parent != tmpCell) {
+                    printf("TEST ERROR: pmCheckParents() did not restore Readout->parent.\n");
+                    testStatus = 4;
+                }
+            }
+        }
+    }
+
+    psFree(tmpFPA);
+    return(testStatus);
+}
+
+/******************************************************************************
+test5(): This routine wil test the pmFPASelectChip() and pmFPAExcludeChip()
+functions.  We generate an pmFPA hierarchy, then set the ->valid members with
+those routines, then verify.
+ *****************************************************************************/
+psS32 test5( void )
+{
+    psS32 testStatus = 0;
+    pmChip *tmpChip = NULL;
+
+    //
+    // Generate a pmFPA hierarchy.
+    //
+    pmFPA *tmpFPA = genSystem();
+
+    //
+    // We test the ->valid member for each chip.
+    //
+    for (psS32 i = 0 ; i < tmpFPA->chips->n ; i++) {
+        tmpChip = (pmChip *) tmpFPA->chips->data[i];
+        if ((tmpChip == NULL) || (tmpChip->valid != false)) {
+            printf("TEST ERROR: Could not properly generate an FPA hierarchy.\n");
+            testStatus = 1;
+        }
+    }
+
+    //
+    // Exclude chip number 0, include all others, then test return value
+    //
+    psS32 numChips = pmFPAExcludeChip(tmpFPA, 0);
+    if (numChips != (NUM_CHIPS-1)) {
+        printf("TEST ERROR: pmFPAExcludeChip() did not return the correct number of chips.\n");
+        testStatus = 2;
+    }
+
+    //
+    // We test the ->valid member for each chip.
+    //
+    tmpChip = (pmChip *) tmpFPA->chips->data[0];
+    if (tmpChip->valid != false) {
+        printf("TEST ERROR: pmFPAExcludeChip() did not set the proper chip->valid to FALSE.\n");
+        testStatus = 3;
+    }
+    for (psS32 i = 1 ; i < tmpFPA->chips->n ; i++) {
+        pmChip *tmpChip = (pmChip *) tmpFPA->chips->data[i];
+        if (tmpChip->valid != true) {
+            printf("TEST ERROR: pmFPAExcludeChip() did not set the proper chip->valids to FALSE.\n");
+            testStatus = 4;
+        }
+    }
+
+
+    //
+    // Include chip number 0, exclude all others, then test return value
+    //
+    psBool tmpBool = pmFPASelectChip(tmpFPA, 0);
+    if (tmpBool != true) {
+        printf("TEST ERROR: pmFPASelectChip() returned FALSE.\n");
+        testStatus = 5;
+    }
+
+    //
+    // We test the ->valid member for each chip.
+    //
+    tmpChip = (pmChip *) tmpFPA->chips->data[0];
+    if (tmpChip->valid != true) {
+        printf("TEST ERROR: pmFPASelectChip() did not set the proper chip->valid to FALSE.\n");
+        testStatus = 6;
+    }
+    for (psS32 i = 1 ; i < tmpFPA->chips->n ; i++) {
+        pmChip *tmpChip = (pmChip *) tmpFPA->chips->data[i];
+        if (tmpChip->valid != false) {
+            printf("TEST ERROR: pmFPASelectChip() did not set the proper chip->valids to FALSE.\n");
+            testStatus = 7;
+        }
+    }
+
+    psFree(tmpFPA);
+    return(testStatus);
+}
Index: /branches/eam_branch_20071023/psModules/test/camera/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/.cvsignore	(revision 15358)
@@ -0,0 +1,6 @@
+.deps
+.libs
+Makefile
+Makefile.in
+temp
+tst_pmAstrometry
Index: /branches/eam_branch_20071023/psModules/test/camera/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/Makefile.am	(revision 15358)
@@ -0,0 +1,38 @@
+AM_CPPFLAGS = \
+	$(SRCINC) \
+	-I$(top_srcdir)/test/tap/src \
+	-I$(top_srcdir)/test/pstap/src \
+	$(PSMODULES_CFLAGS)
+
+AM_LDFLAGS = \
+	$(top_builddir)/src/libpsmodules.la  \
+	$(top_builddir)/test/tap/src/libtap.la \
+	$(top_builddir)/test/pstap/src/libpstap.la \
+	$(PSMODULES_LIBS)
+
+TEST_PROGS = \
+	tap_pmFPA \
+	tap_pmFPAReadWrite \
+	tap_pmHDU \
+	tap_pmHDUUtils \
+	tap_pmFPALevel \
+	tap_pmFPAFlags \
+	tap_pmFPAExtent \
+	tap_pmFPAUtils \
+	tap_pmFPAConstruct \
+	tap_pmFPAView \
+	tap_pmFPAHeader \
+	tap_pmFPAMaskW \
+	tap_pmFPACellSquish
+
+if BUILD_TESTS
+bin_PROGRAMS = $(TEST_PROGS)
+TESTS = $(TEST_PROGS)
+else
+check_PROGRAMS = $(TEST_PROGS)
+endif
+
+CLEANFILES = $(check_DATA) temp/* core core.* *~ *.bb *.bbg *.da gmon.out
+
+test: check
+	$(top_srcdir)/test/test.pl
Index: /branches/eam_branch_20071023/psModules/test/camera/data/SampleIPPConfig
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/SampleIPPConfig	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/SampleIPPConfig	(revision 15358)
@@ -0,0 +1,45 @@
+### Example .ipprc file
+    PATH            STR     .
+    DATAPATH	str	datapath
+
+### Database configuration
+    DBSERVER	STR	localhost
+    DBUSER		STR	test
+    DBPASSWORD	STR	""
+    DBNAME          STR     test
+    DBPORT		S32	0
+
+### Setups for each camera system
+    CAMERAS		METADATA
+	CAMERA0		STR	camera0/camera.config
+	CAMERA1		STR	camera1/camera.config
+    END
+
+### Setups for psLib
+    TIME		STR	time.config
+    LOGLEVEL	S32	3
+    LOGFORMAT	STR	HLNM
+    LOGDEST	STR	STDOUT
+
+### Default trace logging initializations
+    TRACE		METADATA
+	dummyTraceFacility01	S32	1
+	dummyTraceFacility02	S32	2
+    END
+
+### Predefined recipe files
+    RECIPES         METADATA                # Site-level recipes
+        R00		STR	recipes/R00.config
+        R01		STR	recipes/R01.config
+        R02		STR	recipes/R02.config
+        R03		STR	recipes/R03.config
+    END
+
+### Misc arbitraty metadata
+    ARBITRARY_STRING_S32	S32	20
+    ARBITRARY_STRING_F32	F32	21.0
+    ARBITRARY_STRING_F64	F64	22.0
+    ARBITRARY_STRING_STR	STR	19.0
+    ARBITRARY_STRING_BOOL_T	BOOL	true
+    ARBITRARY_STRING_BOOL_F	BOOL	false
+
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera0/#camera.config~
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera0/#camera.config~	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera0/#camera.config~	(revision 15358)
@@ -0,0 +1,22 @@
+FORMATS         METADATA
+        C0_FM0     STR     camera0/format0.config
+        C0_FM1     STR     camera0/format1.config
+END
+
+RECIPES         METADATA
+        C0_RECIPE0          STR     camera0/recipe0.config
+        C0_RECIPE1          STR     camera0/recipe1.config
+END
+
+# How to translate PS concepts into FITS headers
+TRANSLATION     METADATA
+        CELL.TRIMSEC            STR     TRIMSEC
+        CELL.BIASSEC            STR     BIASSEC
+END
+
+
+FPA     METADATA
+        ccd00   STR     LeftAmp RightAmp
+        ccd01   STR     LeftAmp RightAmp
+END
+ID      STR     CAMERA0
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera0/camera.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera0/camera.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera0/camera.config	(revision 15358)
@@ -0,0 +1,24 @@
+FORMATS         METADATA
+        C0_FM0     STR     camera0/format0.config
+        C0_FM1     STR     camera0/format1.config
+END
+
+RECIPES         METADATA
+        C0_RECIPE0          STR     camera0/recipe0.config
+        C0_RECIPE1          STR     camera0/recipe1.config
+END
+
+# How to translate PS concepts into FITS headers
+TRANSLATION     METADATA
+        CELL.TRIMSEC            STR     TRIMSEC
+        CELL.BIASSEC            STR     BIASSEC
+        CELL.TRIMSEC0            STR     TRIMSEC
+        CELL.BIASSEC1            STR     BIASSEC
+END
+
+
+FPA     METADATA
+        ccd00   STR     LeftAmp RightAmp
+        ccd01   STR     LeftAmp RightAmp
+END
+ID      STR     CAMERA0
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera0/format0.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera0/format0.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera0/format0.config	(revision 15358)
@@ -0,0 +1,31 @@
+RULE    METADATA
+        F0_KEY0        STR     string20
+        F0_KEY1        STR     string21
+        F0_KEY2        S32     20
+END
+
+FILE    METADATA
+        PHU             STR     FPA
+        EXTENSIONS      STR     CELL
+        FPA.NAME        STR     EXPNUM
+END
+
+DEFAULTS        METADATA
+        F0_DEF0           STR     DefString20
+        F0_DEF1           STR     DefString21
+        F0_DEF2           F32     21.0
+        F0_DEF3           S32     22
+END
+
+CELLS   METADATA
+        left    METADATA        # Left amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+        right   METADATA        # Right amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+END
+
+ID	STR	camera0/format0.config
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera0/format1.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera0/format1.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera0/format1.config	(revision 15358)
@@ -0,0 +1,29 @@
+RULE    METADATA
+        F1_KEY0        STR     string30
+        F1_KEY1        STR     string31
+        F1_KEY2        S32     30
+END
+
+FILE    METADATA
+        PHU             STR     FPA
+        EXTENSIONS      STR     CELL
+        FPA.NAME        STR     EXPNUM
+END
+
+DEFAULTS        METADATA
+        F1_DEF0           STR     DefString30
+        F1_DEF1           STR     DefString31
+        F1_DEF2           F32     31.0
+        F1_DEF3           S32     32
+END
+
+CELLS   METADATA
+        left    METADATA        # Left amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+        right   METADATA        # Right amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+END
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera0/recipe0.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera0/recipe0.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera0/recipe0.config	(revision 15358)
@@ -0,0 +1,4 @@
+R0_KEY0		STR	RecipeString0
+R0_KEY1		STR	RecipeString1
+R0_KEY2		S32	2
+R0_KEY3		F32	3
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera0/recipe1.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera0/recipe1.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera0/recipe1.config	(revision 15358)
@@ -0,0 +1,4 @@
+R1_KEY0		STR	RecipeString10
+R1_KEY1		STR	RecipeString11
+R1_KEY2		S32	12
+R1_KEY3		F32	13
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera1/#camera.config~
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera1/#camera.config~	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera1/#camera.config~	(revision 15358)
@@ -0,0 +1,14 @@
+FORMATS         METADATA
+        C1_FM0     STR     camera1/format0.config
+        C1_FM1     STR     camera1/format1.config
+END
+
+RECIPES         METADATA
+        C1_RECIPE0          STR     camera1/recipe0.config
+        C1_RECIPE1          STR     camera1/recipe1.config
+END
+
+FPA     METADATA
+        ccd00   STR     LeftAmp RightAmp
+        ccd01   STR     LeftAmp RightAmp
+END
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera1/camera.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera1/camera.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera1/camera.config	(revision 15358)
@@ -0,0 +1,20 @@
+FORMATS         METADATA
+        C1_FM0     STR     camera1/format0.config
+        C1_FM1     STR     camera1/format1.config
+END
+
+RECIPES         METADATA
+        C1_RECIPE0          STR     camera1/recipe0.config
+        C1_RECIPE1          STR     camera1/recipe1.config
+END
+
+# How to translate PS concepts into FITS headers
+TRANSLATION     METADATA
+        CELL.TRIMSEC            STR     TRIMSEC
+        CELL.BIASSEC            STR     BIASSEC
+END
+
+FPA     METADATA
+        ccd00   STR     LeftAmp RightAmp
+        ccd01   STR     LeftAmp RightAmp
+END
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera1/format0.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera1/format0.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera1/format0.config	(revision 15358)
@@ -0,0 +1,29 @@
+RULE    METADATA
+        F0_KEY0        STR     string40
+        F0_KEY1        STR     string41
+        F0_KEY2        S32     420
+END
+
+FILE    METADATA
+        PHU             STR     FPA
+        EXTENSIONS      STR     CELL
+        FPA.NAME        STR     EXPNUM
+END
+
+DEFAULTS        METADATA
+        F0_DEF0           STR     DefString40
+        F0_DEF1           STR     DefString41
+        F0_DEF2           F32     41.0
+        F0_DEF3           S32     44
+END
+
+CELLS   METADATA
+        left    METADATA        # Left amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+        right   METADATA        # Right amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+END
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera1/format1.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera1/format1.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera1/format1.config	(revision 15358)
@@ -0,0 +1,29 @@
+RULE    METADATA
+        F1_KEY0        STR     string80
+        F1_KEY1        STR     string81
+        F1_KEY2        S32     80
+END
+
+FILE    METADATA
+        PHU             STR     FPA
+        EXTENSIONS      STR     CELL
+        FPA.NAME        STR     EXPNUM
+END
+
+DEFAULTS        METADATA
+        F1_DEF0           STR     DefString80
+        F1_DEF1           STR     DefString81
+        F1_DEF2           F32     81.0
+        F1_DEF3           S32     82
+END
+
+CELLS   METADATA
+        left    METADATA        # Left amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+        right   METADATA        # Right amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+END
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera1/recipe0.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera1/recipe0.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera1/recipe0.config	(revision 15358)
@@ -0,0 +1,4 @@
+R0_KEY0		STR	RecipeString120
+R0_KEY1		STR	RecipeString121
+R0_KEY2		S32	122
+R0_KEY3		F32	123
Index: /branches/eam_branch_20071023/psModules/test/camera/data/camera1/recipe1.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/camera1/recipe1.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/camera1/recipe1.config	(revision 15358)
@@ -0,0 +1,4 @@
+R1_KEY0		STR	RecipeString230
+R1_KEY1		STR	RecipeString2311
+R1_KEY2		S32	232
+R1_KEY3		F32	233
Index: /branches/eam_branch_20071023/psModules/test/camera/data/path2/SampleIPPConfig2
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/path2/SampleIPPConfig2	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/path2/SampleIPPConfig2	(revision 15358)
@@ -0,0 +1,32 @@
+### Example .ipprc file
+
+### Database configuration
+DBSERVER	STR	ippdb.ifa.hawaii.edu	# Database host name (for psDBInit)
+DBUSER		STR	ipp			# Database user name (for psDBInit)
+DBPASSWORD	STR	password		# Database password (for psDBInit)
+
+### Setups for each camera system
+CAMERAS		METADATA
+	MEGACAM_RAW	STR	megacam_raw.config
+	MEGACAM_SPLICE	STR	megacam_splice.config
+	GPC1_RAW	STR	gpc1_raw.config
+	LRIS_BLUE	STR	lris_blue.config
+	LRIS_RED	STR	lris_red.config
+END
+
+### psLib setup
+#TIME		STR	/home/mithrandir/price/pan-starrs/jhroot/i686-pc-linux-gnu/etc/pslib/psTime.config	# Time configuration file
+LOGLEVEL	S32	3			# Logging level; 3=INFO
+LOGFORMAT	STR	HLNM			# Log format
+LOGDEST	STR	STDOUT				# Log destination
+TRACE		METADATA			# Trace levels
+	dummyTraceFunc01	S32	1
+	dummyTraceFunc02	S32	2
+END
+ARBITRARY_STRING_S32	S32	20
+ARBITRARY_STRING_F32	F32	21.0
+ARBITRARY_STRING_F64	F64	22.0
+ARBITRARY_STRING_STR	STR	19.0
+ARBITRARY_STRING_BOOL_T	BOOL	true
+ARBITRARY_STRING_BOOL_F	BOOL	false
+
Index: /branches/eam_branch_20071023/psModules/test/camera/data/sampleFitsFile.fits
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/data/sampleFitsFile.fits	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/data/sampleFitsFile.fits	(revision 15358)
@@ -0,0 +1,1 @@
+SIMPLE  =                    T / file does conform to FITS standard             BITPIX  =                  -32 / number of bits per data pixel                  NAXIS   =                    2 / number of data axes                            NAXIS1  =                   16 / length of data axis 1                          NAXIS2  =                   16 / length of data axis 2                          EXTEND  =                    T / FITS dataset may contain extensions            COMMENT   FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT   and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H MYINT   =                    0 / psS32 Item                                     MYFLT   =                   1. / psF32 Item                                     MYDBL   =                   1. / psF64 Item                                     MYBOOL  =                    T / psBool Item                                    MYSTR   = 'ext-0   '           / String Item                                    EXTNAME = 'ext-0   '                                                            END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             XTENSION= 'IMAGE   '           / IMAGE extension                                BITPIX  =                  -32 / number of bits per data pixel                  NAXIS   =                    2 / number of data axes                            NAXIS1  =                   16 / length of data axis 1                          NAXIS2  =                   16 / length of data axis 2                          PCOUNT  =                    0 / required keyword; must = 0                     GCOUNT  =                    1 / required keyword; must = 1                     MYINT   =                    1 / psS32 Item                                     MYFLT   =                  0.5 / psF32 Item                                     MYDBL   =                  0.5 / psF64 Item                                     MYBOOL  =                    F / psBool Item                                    MYSTR   = 'ext-1   '           / String Item                                    EXTNAME = 'ext-1   '                                                            END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  XTENSION= 'IMAGE   '           / IMAGE extension                                BITPIX  =                  -32 / number of bits per data pixel                  NAXIS   =                    2 / number of data axes                            NAXIS1  =                   16 / length of data axis 1                          NAXIS2  =                   16 / length of data axis 2                          PCOUNT  =                    0 / required keyword; must = 0                     GCOUNT  =                    1 / required keyword; must = 1                     MYINT   =                    2 / psS32 Item                                     MYFLT   =            0.3333333 / psF32 Item                                     MYDBL   =    0.333333333333333 / psF64 Item                                     MYBOOL  =                    T / psBool Item                                    MYSTR   = 'ext-2   '           / String Item                                    EXTNAME = 'ext-2   '                                                            END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   XTENSION= 'IMAGE   '           / IMAGE extension                                BITPIX  =                  -32 / number of bits per data pixel                  NAXIS   =                    2 / number of data axes                            NAXIS1  =                   16 / length of data axis 1                          NAXIS2  =                   16 / length of data axis 2                          PCOUNT  =                    0 / required keyword; must = 0                     GCOUNT  =                    1 / required keyword; must = 1                     MYINT   =                    3 / psS32 Item                                     MYFLT   =                 0.25 / psF32 Item                                     MYDBL   =                 0.25 / psF64 Item                                     MYBOOL  =                    F / psBool Item                                    MYSTR   = 'ext-3   '           / String Item                                    EXTNAME = 'ext-3   '                                                            END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@  @@                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  XTENSION= 'IMAGE   '           / IMAGE extension                                BITPIX  =                  -32 / number of bits per data pixel                  NAXIS   =                    2 / number of data axes                            NAXIS1  =                   16 / length of data axis 1                          NAXIS2  =                   16 / length of data axis 2                          PCOUNT  =                    0 / required keyword; must = 0                     GCOUNT  =                    1 / required keyword; must = 1                     MYINT   =                    4 / psS32 Item                                     MYFLT   =                  0.2 / psF32 Item                                     MYDBL   =                  0.2 / psF64 Item                                     MYBOOL  =                    T / psBool Item                                    MYSTR   = 'ext-4   '           / String Item                                    EXTNAME = 'ext-4   '                                                            END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @  @                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  XTENSION= 'IMAGE   '           / IMAGE extension                                BITPIX  =                  -32 / number of bits per data pixel                  NAXIS   =                    2 / number of data axes                            NAXIS1  =                   16 / length of data axis 1                          NAXIS2  =                   16 / length of data axis 2                          PCOUNT  =                    0 / required keyword; must = 0                     GCOUNT  =                    1 / required keyword; must = 1                     MYINT   =                    5 / psS32 Item                                     MYFLT   =            0.1666667 / psF32 Item                                     MYDBL   =    0.166666666666667 / psF64 Item                                     MYBOOL  =                    F / psBool Item                                    MYSTR   = 'ext-5   '           / String Item                                    EXTNAME = 'ext-5   '                                                            END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @   @                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   XTENSION= 'IMAGE   '           / IMAGE extension                                BITPIX  =                  -32 / number of bits per data pixel                  NAXIS   =                    2 / number of data axes                            NAXIS1  =                   16 / length of data axis 1                          NAXIS2  =                   16 / length of data axis 2                          PCOUNT  =                    0 / required keyword; must = 0                     GCOUNT  =                    1 / required keyword; must = 1                     MYINT   =                    6 / psS32 Item                                     MYFLT   =            0.1428571 / psF32 Item                                     MYDBL   =    0.142857142857143 / psF64 Item                                     MYBOOL  =                    T / psBool Item                                    MYSTR   = 'ext-6   '           / String Item                                    EXTNAME = 'ext-6   '                                                            END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À  @À                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  XTENSION= 'IMAGE   '           / IMAGE extension                                BITPIX  =                  -32 / number of bits per data pixel                  NAXIS   =                    2 / number of data axes                            NAXIS1  =                   16 / length of data axis 1                          NAXIS2  =                   16 / length of data axis 2                          PCOUNT  =                    0 / required keyword; must = 0                     GCOUNT  =                    1 / required keyword; must = 1                     MYINT   =                    7 / psS32 Item                                     MYFLT   =                0.125 / psF32 Item                                     MYDBL   =                0.125 / psF64 Item                                     MYBOOL  =                    F / psBool Item                                    MYSTR   = 'ext-7   '           / String Item                                    EXTNAME = 'ext-7   '                                                            END                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à  @à                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPA.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPA.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPA.c	(revision 15358)
@@ -0,0 +1,409 @@
+/** @file  tst_pmAstrometry.c
+ *
+ *  XXX: Untested: pmFPACheckParents()
+ *
+    void pmCellFreeReadouts(pmCell *cell)
+    void pmCellFreeData(pmCell *cell)
+    void pmChipFreeCells(pmChip *chip)   
+    void pmChipFreeData(pmChip *chip)
+    void pmFPAFreeData(pmFPA *fpa)
+    void pmReadoutFreeData(pmReadout *readout)
+
+*    pmReadout *pmReadoutAlloc(pmCell *cell)
+*    pmCell *pmCellAlloc(pmChip *chip, const char *name)
+*    pmChip *pmChipAlloc(pmFPA *fpa, const char *name)
+*    pmFPA *pmFPAAlloc(const psMetadata *camera)
+
+    bool pmFPACheckParents(pmFPA *fpa)
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-18 18:57:18 $
+ *
+ *  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+#define CHIP_ALLOC_NAME "ChipName"
+#define CELL_ALLOC_NAME "CellName"
+#define MISC_NUM 32
+#define MISC_NAME "META00"
+#define MISC_NAME2 "META01"
+#define NUM_BIAS_DATA 10
+#define TEST_NUM_ROWS 32
+#define TEST_NUM_COLS 32
+#define NUM_READOUTS	4
+#define NUM_CELLS	6
+#define NUM_CHIPS	8
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    cell->hdu = pmHDUAlloc(NULL);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    chip->hdu = pmHDUAlloc(NULL);
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip);
+    }
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    fpa->hdu = pmHDUAlloc(NULL);
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa);
+    }
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    plan_tests(56);
+
+    // ------------------------------------------------------------------------
+    // pmFPAAlloc() tests
+    // XXX: Add tests for NULL inputs.
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = pmFPAAlloc(camera);
+        ok(fpa != NULL, "pmFPAAlloc() returned a non-NULL");
+        ok(fpa->fromTPA == NULL, "pmFPAAlloc() set ->fromTPA to NULL");
+        ok(fpa->toTPA == NULL, "pmFPAAlloc() set ->toTPA to NULL");
+        ok(fpa->toSky == NULL, "pmFPAAlloc() set ->toSky to NULL");
+        ok(fpa->concepts != NULL &&
+           psMemCheckMetadata(fpa->concepts), "pmFPAAlloc() set ->concepts correctly");
+        ok(fpa->conceptsRead == PM_CONCEPT_SOURCE_NONE, "pmFPAAlloc() set ->conceptsRead correctly");
+        ok(fpa->analysis != NULL &&
+           psMemCheckMetadata(fpa->analysis), "pmFPAAlloc() set ->analysis correctly");
+        ok(fpa->camera == camera, "pmFPAAlloc() set ->camera correctly");
+        ok(fpa->chips != NULL &&
+           psMemCheckArray(fpa->chips), "pmFPAAlloc() set ->chips correctly");
+        ok(fpa->hdu == NULL, "pmFPAAlloc() set ->hdu to NULL");
+//        ok(fpa->wrote_phu == false, "pmFPAAlloc() set ->wrote_phu to FALSE");
+        psFree(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Populate the pmFPA struct with real data to ensure they were psFree()'ed correctly.
+    {
+        psMemId id = psMemGetId();
+        pmFPA* fpa = generateSimpleFPA(NULL);
+
+        myFreeFPA(fpa);
+/*
+        if (1) {
+            for (int i = 0 ; i < fpa->chips->n ; i++) {
+                pmChip *chip = fpa->chips->data[i];
+                for (int j = 0 ; j < chip->cells->n ; j++) {
+                    pmCell *cell = chip->cells->data[j];
+                    for (int k = 0 ; k < cell->readouts->n ; k++) {
+                        pmReadout *readout = cell->readouts->data[k];
+                        psFree(readout);
+		    }
+                    psFree(cell);
+    		}
+                psFree(chip);
+	    }
+            psFree(fpa);
+	} else {
+            for (int i = 0 ; i < fpa->chips->n ; i++) {
+                pmChip *chip = fpa->chips->data[i];
+                pmChipFreeCells(chip);
+                pmChipFreeData(chip);
+                psFree(chip);
+	    }
+            psFree(fpa);
+	}
+*/
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // ------------------------------------------------------------------------
+    // pmFPAFreeData() tests
+    // XXX: pmFPAAlloc(), pmChipAlloc(), etc, allocate much of the members of
+    // those data structures.  We don't really check that those members were
+    // actually psFree()'ed; we merely ensure that they were set to NULL.
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+//        pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+//        pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+//        pmReadout *readout = pmReadoutAlloc(cell);
+        pmFPAFreeData(fpa);
+        ok(fpa->hdu->images == NULL, "pmFPAFreeData() correctly set fpa->hdu->images to NULL");
+        ok(fpa->hdu->weights == NULL, "pmFPAFreeData() correctly set fpa->hdu->weights to NULL");
+        ok(fpa->hdu->masks == NULL, "pmFPAFreeData() correctly set fpa->hdu->masks to NULL");
+     
+//        ok(chip->hdu->images == NULL, "pmFPAFreeData() correctly set chip->hdu->images to NULL");
+//        ok(chip->hdu->weights == NULL, "pmFPAFreeData() correctly set chip->hdu->weights to NULL");
+//        ok(chip->hdu->masks == NULL, "pmFPAFreeData() correctly set chip->hdu->masks to NULL");
+//        ok(cell->hdu->images == NULL, "pmFPAFreeData() correctly set cell->hdu->images to NULL");
+//        ok(cell->hdu->weights == NULL, "pmFPAFreeData() correctly set cell->hdu->weights to NULL");
+//        ok(cell->hdu->masks == NULL, "pmFPAFreeData() correctly set cell->hdu->masks to NULL");
+        psFree(camera);
+        psFree(fpa);
+//        psFree(chip);
+//        psFree(cell);
+//        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // ------------------------------------------------------------------------
+    // pmChipAlloc() tests
+    // XXX: Add tests for NULL inputs.
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+        ok(chip != NULL, "pmChipAlloc() returned non-NULL");
+        ok(chip->toFPA == NULL, "pmChipAlloc() set chip->toChip to NULL");
+        ok(chip->fromFPA == NULL, "pmChipAlloc() set chip->fromChip to NULL");
+        ok(chip->concepts != NULL &&
+           psMemCheckMetadata(chip->concepts), "pmChipAlloc() set ->concepts correctly");
+        ok(chip->conceptsRead == PM_CONCEPT_SOURCE_NONE, "pmCellAlloc() set ->conceptsRead correctly");
+        ok(chip->analysis != NULL &&
+           psMemCheckMetadata(chip->analysis), "pmChipAlloc() set ->analysis correctly");
+        ok(chip->cells != NULL &&
+           psMemCheckArray(chip->cells), "pmChipAlloc() set ->cells correctly");
+        ok(chip->parent == fpa, "pmChipAlloc() set ->parent correctly");
+        ok(chip->process == true, "pmChipAlloc() set ->process correctly");
+        ok(chip->file_exists == false, "pmChipAlloc() set ->file_exists correctly");
+        ok(chip->data_exists == false, "pmChipAlloc() set ->data_exists correctly");
+        ok(chip->hdu == NULL, "pmChipAlloc() set ->hdu to NULL");
+//        ok(chip->wrote_phu == false, "pmChipAlloc() set ->wrote_phu correctly");
+        psFree(camera);
+        psFree(fpa);
+        psFree(chip);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Populate the pmChip struct with real data to ensure they were free'ed correctly.
+    {
+        psMemId id = psMemGetId();
+        pmChip *chip = generateSimpleChip(NULL);
+        psFree(chip);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmCellAlloc() tests
+    // XXX: Add tests for NULL inputs.
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+        pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+        ok(cell != NULL, "pmCellAlloc returned non-NULL");
+        ok(cell->concepts != NULL &&
+           psMemCheckMetadata(cell->concepts), "pmCellpAlloc() set ->concepts correctly");
+        ok(cell->conceptsRead == PM_CONCEPT_SOURCE_NONE, "pmCellAlloc() set ->conceptsRead correctly");
+        ok(cell->config == NULL, "pmCellpAlloc() set ->config to NULL");
+        ok(cell->analysis != NULL &&
+           psMemCheckMetadata(cell->analysis), "pmCellAlloc() set ->analysis correctly");
+        ok(cell->readouts != NULL &&
+           psMemCheckArray(cell->readouts), "pmCellAlloc() set ->readouts correctly");
+        ok(cell->parent == chip, "pmCellAlloc() set ->parent correctly");
+        ok(cell->process == true, "pmCellAlloc() set ->process correctly");
+        ok(cell->file_exists == false, "pmCellAlloc() set ->file_exists correctly");
+        ok(cell->data_exists == false, "pmCellAlloc() set ->data_exists correctly");
+        ok(cell->hdu == NULL, "pmCellAlloc() set ->hdu to NULL");
+//        ok(cell->wrote_phu == false, "pmCellAlloc() set ->wrote_phu correctly");
+        psFree(camera);
+        psFree(fpa);
+        psFree(chip);
+        psFree(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Populate the pmCell struct with real data to ensure they were
+    // psFree()'ed correctly.
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        psFree(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test pmCellFreeReadouts()
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmCellFreeReadouts(cell);
+        ok(cell->readouts->n == 0, "pmCellFreeReadouts() correctly set cell->readouts->n to 0");
+        psFree(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks (pmCellFreeReadouts)");
+    }
+
+
+    // ------------------------------------------------------------------------
+    // pmReadoutAlloc() tests
+    // XXX: Add tests for NULL inputs.
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+        pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+        pmReadout *readout = pmReadoutAlloc(cell);
+        ok(readout != NULL, "pmReadoutAlloc() returned non-NULL");
+        ok(readout->col0 == 0, "pmReadoutAlloc() set ->col0 correctly");
+        ok(readout->row0 == 0, "pmReadoutAlloc() set ->row0 correctly");
+        ok(readout->image == NULL, "pmReadoutAlloc() set ->image correctly");
+        ok(readout->mask == NULL, "pmReadoutAlloc() set ->mask correctly");
+        ok(readout->weight == NULL, "pmReadoutAlloc() set ->weight correctly");
+        ok(readout->bias != NULL &&
+           psMemCheckList(readout->bias), "pmReadoutAlloc() set ->bias correctly");
+        ok(readout->analysis != NULL &&
+           psMemCheckMetadata(readout->analysis), "pmReadoutAlloc() set ->analysis correctly");
+        ok(readout->parent == cell, "pmReadoutAlloc() set ->parent correctly");
+        ok(readout->process == true, "pmReadoutAlloc() set ->process correctly");
+        ok(readout->file_exists == false, "pmReadoutAlloc() set ->file_exists correctly");
+        ok(readout->data_exists == false, "pmReadoutAlloc() set ->data_exists correctly");
+        psFree(camera);
+        psFree(fpa);
+        psFree(chip);
+        psFree(cell);
+        psFree(readout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Populate the pmReadout struct with real data to ensure they were
+    // psFree()'ed correctly.
+    {
+        psMemId id = psMemGetId();
+        pmReadout *readout = generateSimpleReadout(NULL);
+        psFree(readout);
+        // XXX: The pmReadout->bias list is not being free'ed correctly.
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+/*
+    {
+        psMemId id = psMemGetId();
+        pmReadout *readout = pmReadoutAlloc(NULL);
+        readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+        readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+            psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+            psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        }
+        psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+        return(readout);
+    }
+*/
+
+
+}
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPACellSquish.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPACellSquish.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPACellSquish.c	(revision 15358)
@@ -0,0 +1,237 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+// XXX: Use better name for the temporary FITS file
+// XXX: The code to generate and free the FPA hierarchy was copied from
+// tap-pmFPA.c.  EIther include it directly, or library, or something.
+// Also, get rid of the manual free functions and use psFree() once
+// it correctly frees child members
+// XXX: For the genSimpleFPA() code, add IDs to each function so that
+// the values set in each chip-?cell-?hdu-?image are unique
+// XXX: For the genSimpleFPA() code, write masks and weights as well
+
+#define CHIP_ALLOC_NAME        "ChipName"
+#define CELL_ALLOC_NAME        "CellName"
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define MISC_NAME2             "META01"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           4
+#define TEST_NUM_COLS           4
+#define NUM_READOUTS            3
+#define NUM_CELLS               10
+#define NUM_CHIPS               8
+#define NUM_HDUS                5
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    chip->hdu = pmHDUAlloc("chipExtName");
+//
+//    bool rc = pmConfigFileRead(&chip->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleChip())");
+//    }
+//
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip);
+    }
+
+    // XXX: Add code to initialize chip pmConcepts
+
+
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    fpa->hdu = pmHDUAlloc("fpaExtName");
+//
+//    bool rc = pmConfigFileRead(&fpa->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+//    }
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa);
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+//    bool mdok;
+//    psMetadata *fileData = psMetadataLookupMetadata(&mdok, fpa->hdu->format, "FILE");
+//    char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+//    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, NULL, fpaNameHdr);
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(25);
+
+
+    // ----------------------------------------------------------------------
+    // pmCellSquish() tests
+    // bool pmCellSquish(pmCell *cell, psMaskType maskVal, bool useShifts)
+    {
+        psMemId id = psMemGetId();
+        ok(!pmCellSquish(NULL, 0, false), "pmCellSquish(NULL) returned NULL");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // ----------------------------------------------------------------------
+    // pmCellSquish() tests
+    // bool pmCellSquish(pmCell *cell, psMaskType maskVal, bool useShifts)
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(pmCellSquish(cell, 0, false), "pmCellSquish(NULL) returned NULL");
+        myFreeFPA(fpa);
+        psFree(camera);
+        if (0) psFree(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAConstruct.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAConstruct.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAConstruct.c	(revision 15358)
@@ -0,0 +1,68 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(72);
+
+    // ----------------------------------------------------------------------
+    // pmFPAConstruct() tests
+    // pmFPA *pmFPAConstruct(const psMetadata *camera)
+    // test will NULL camera input param
+    {
+        psMemId id = psMemGetId();
+        pmFPA* fpa = pmFPAConstruct(NULL);
+        ok(fpa == NULL, "pmFPAConstruct() NULL will NULL camera input param");
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // pmFPA *pmFPAConstruct(const psMetadata *camera)
+    // test will acceptable data
+    // XXX: The memory leak code is put outside the call to pmConfigFileRead() because of
+    // a memory leak in pmConfigFileRead().
+    {
+        psMetadata *camera = psMetadataAlloc();
+        bool rc = pmConfigFileRead(&camera, "data/camera0/camera.config", "CAMERA 0 config file");
+        // Generate the pmFPA heirarchy
+        psMemId id = psMemGetId();
+        ok(rc, "Succesfully read camera format file");
+        pmFPA* fpa = pmFPAConstruct(camera);
+        ok(fpa != NULL, "pmFPAConstruct() returned non-NULL");
+        if (VERBOSE) {
+            pmFPAPrint(stdout, fpa, true, true);
+	}
+        bool errorFlag = false;
+        ok(fpa->chips->n == 4, "pmFPAConstruct() set fpa->chips->n (%d)", fpa->chips->n);
+        for (int chipID = 0 ; chipID < fpa->chips->n ; chipID++) {
+            pmChip *chip = fpa->chips->data[chipID];
+            ok(chip->cells->n == 2, "pmFPAConstruct() set chip->cells->n (%d)", chip->cells->n);
+            for (int cellID = 0 ; cellID < chip->cells->n ; cellID++) {
+                pmCell *cell = chip->cells->data[cellID];
+                for (int readoutID = 0 ; readoutID < cell->readouts->n ; readoutID++) {
+                    pmReadout *readout = cell->readouts->data[readoutID];
+                    readout = readout;
+		}
+	    }
+	}
+        ok(!errorFlag, "pmFPAConstruct() read the pmFPA structure correctly");
+
+
+
+
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+
+        psFree(camera);
+    }
+}
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAExtent.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAExtent.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAExtent.c	(revision 15358)
@@ -0,0 +1,474 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+// XXX: Use better name for the temporary FITS file
+// XXX: The code to generate and free the FPA hierarchy was copied from
+// tap-pmFPA.c.  EIther include it directly, or library, or something.
+// Also, get rid of the manual free functions and use psFree() once
+// it correctly frees child members
+// XXX: For the genSimpleFPA() code, add IDs to each function so that
+// the values set in each chip-?cell-?hdu-?image are unique
+// XXX: For the genSimpleFPA() code, write masks and weights as well
+
+#define CHIP_ALLOC_NAME        "ChipName"
+#define CELL_ALLOC_NAME        "CellName"
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define MISC_NAME2             "META01"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           4
+#define TEST_NUM_COLS           4
+#define NUM_READOUTS            3
+#define NUM_CELLS               10
+#define NUM_CHIPS               8
+#define NUM_HDUS                5
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    chip->hdu = pmHDUAlloc("chipExtName");
+//
+//    bool rc = pmConfigFileRead(&chip->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleChip())");
+//    }
+//
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip);
+    }
+
+    // XXX: Add code to initialize chip pmConcepts
+
+
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    fpa->hdu = pmHDUAlloc("fpaExtName");
+//
+//    bool rc = pmConfigFileRead(&fpa->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+//    }
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa);
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+//    bool mdok;
+//    psMetadata *fileData = psMetadataLookupMetadata(&mdok, fpa->hdu->format, "FILE");
+//    char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+//    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, NULL, fpaNameHdr);
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(25);
+
+
+    // ----------------------------------------------------------------------
+    // pmReadoutExtent() tests: NULL input
+    {
+        psMemId id = psMemGetId();
+        ok(NULL == pmReadoutExtent(NULL), "pmReadoutExtent(NULL) returned NULL");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // pmReadoutExtent() tests: acceptable inputs
+    // XXX: We should probably test when the images are NULL, and use different size
+    // images in each readout.
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        bool errorFlag = false;
+        for (int chipID = 0 ; chipID < fpa->chips->n ; chipID++) {
+            pmChip *chip = fpa->chips->data[chipID];
+            for (int cellID = 0 ; cellID < chip->cells->n ; cellID++) {
+                pmCell *cell = chip->cells->data[cellID];
+                for (int readoutID = 0 ; readoutID < cell->readouts->n ; readoutID++) {
+                    pmReadout *readout = cell->readouts->data[readoutID];
+                    psRegion *region = pmReadoutExtent(readout);
+                    int xWindow = psMetadataLookupS32(NULL, readout->parent->concepts, "CELL.XWINDOW");
+                    int yWindow = psMetadataLookupS32(NULL, readout->parent->concepts, "CELL.YWINDOW");
+                    if (!region || 
+                         region->x0 != xWindow ||
+                         region->x1 != xWindow + readout->image->numCols ||
+                         region->y0 != yWindow ||
+                         region->y1 != yWindow + readout->image->numRows) {
+                        diag("ERROR: pmReadoutExtent() did not set the psRegion correctly for chip/cell/readout (%d/%d/%d)\n", chipID, cellID, readoutID);
+                        errorFlag = true;
+		    }
+                    psFree(region);
+		}
+	    }
+	}
+        ok(!errorFlag, "pmReadoutExtent() passed all tests");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmCellExtent() tests: NULL input
+    // psRegion *pmCellExtent(const pmCell *cell)
+    {
+        psMemId id = psMemGetId();
+        ok(NULL == pmCellExtent(NULL), "pmCellExtent(NULL) returned NULL");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmCellExtent() tests: acceptable inputs
+    // XX: We should probably set different region sizes to better test the min/max code
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        bool errorFlag = false;
+        for (int chipID = 0 ; chipID < fpa->chips->n ; chipID++) {
+            pmChip *chip = fpa->chips->data[chipID];
+            for (int cellID = 0 ; cellID < chip->cells->n ; cellID++) {
+                pmCell *cell = chip->cells->data[cellID];
+                // Determine the actual extent
+                psRegion *cellExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of cell
+                for (int readoutID = 0 ; readoutID < cell->readouts->n ; readoutID++) {
+                    pmReadout *readout = cell->readouts->data[readoutID];
+                    psRegion *roExtent = pmReadoutExtent(readout); // Extent of readout
+                    cellExtent->x0 = PS_MIN(cellExtent->x0, roExtent->x0);
+                    cellExtent->x1 = PS_MAX(cellExtent->x1, roExtent->x1);
+                    cellExtent->y0 = PS_MIN(cellExtent->y0, roExtent->y0);
+                    cellExtent->y1 = PS_MAX(cellExtent->y1, roExtent->y1);
+                    psFree(roExtent);
+		}
+                bool mdok;
+                int x0 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.X0"); // Cell x offset
+                int y0 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.Y0"); // Cell y offset
+                cellExtent->x0 += x0;
+                cellExtent->x1 += x0;
+                cellExtent->y0 += y0;
+                cellExtent->y1 += y0;
+    
+                psRegion *tstExtent = pmCellExtent(cell);
+                if (!cell ||
+                     tstExtent->x0 != cellExtent->x0 ||
+                     tstExtent->x1 != cellExtent->x1 ||
+                     tstExtent->y0 != cellExtent->y0 ||
+                     tstExtent->y1 != cellExtent->y1) {
+                     diag("ERROR: psRegion set incorrectly for chip/cell (%d/%d)", chipID, cellID);
+                     errorFlag = true;
+		}
+                psFree(tstExtent);
+                psFree(cellExtent);
+	    }
+	}
+        ok(!errorFlag, "pmCellExtent() passed all tests");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmChipExtent() tests: NULL input
+    // psRegion *pmChipExtent(const pmChip *chip)
+    {
+        psMemId id = psMemGetId();
+        ok(NULL == pmChipExtent(NULL), "pmChipExtent(NULL) returned NULL");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmChipExtent() tests: acceptable inputs
+    // XX: We should probably set different region sizes to better test the min/max code
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        bool errorFlag = false;
+        for (int chipID = 0 ; chipID < fpa->chips->n ; chipID++) {
+            pmChip *chip = fpa->chips->data[chipID];
+            psRegion *chipExtent = pmChipPixels(chip);
+            bool rc;
+            int x0 = psMetadataLookupS32(&rc, chip->concepts, "CHIP.X0"); // Chip x offset
+            int y0 = psMetadataLookupS32(&rc, chip->concepts, "CHIP.Y0"); // Chip y offset
+            chipExtent->x0 += x0;
+            chipExtent->x1 += x0;
+            chipExtent->y0 += y0;
+            chipExtent->y1 += y0;
+            psRegion *tstExtent = pmChipExtent(chip);
+            if (!chip ||
+                tstExtent->x0 != chipExtent->x0 ||
+                tstExtent->x1 != chipExtent->x1 ||
+                tstExtent->y0 != chipExtent->y0 ||
+                tstExtent->y1 != chipExtent->y1) {
+                diag("ERROR: psRegion set incorrectly for chip (%d)", chipID);
+                errorFlag = true;
+	    }
+            psFree(tstExtent);
+            psFree(chipExtent);
+	}
+        ok(!errorFlag, "pmChipExtent() passed all tests");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmChipPixels() tests: NULL input
+    // psRegion *pmChipPixels(const pmChip *chip)
+    {
+        psMemId id = psMemGetId();
+        ok(NULL == pmChipPixels(NULL), "pmChipPixels(NULL) returned NULL");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmChipPixels() tests: acceptable inputs
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+
+        bool errorFlag = false;
+        for (int chipID = 0 ; chipID < fpa->chips->n ; chipID++) {
+            pmChip *chip = fpa->chips->data[chipID];
+            // Determine the actual pixels
+            psRegion *actualExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0);
+            for (int cellID = 0 ; cellID < chip->cells->n ; cellID++) {
+                pmCell *cell = chip->cells->data[cellID];
+                psRegion *cellExtent = pmCellExtent(cell);
+                actualExtent->x0 = PS_MIN(actualExtent->x0, cellExtent->x0);
+                actualExtent->x1 = PS_MAX(actualExtent->x1, cellExtent->x1);
+                actualExtent->y0 = PS_MIN(actualExtent->y0, cellExtent->y0);
+                actualExtent->y1 = PS_MAX(actualExtent->y1, cellExtent->y1);
+                psFree(cellExtent);
+	    }
+
+            // Now test if pmChipPixels() determines the same pixels
+            psRegion *tstExtent = pmChipPixels(chip);
+            if (!tstExtent ||
+                 tstExtent->x0 != actualExtent->x0 ||
+                 tstExtent->x1 != actualExtent->x1 ||
+                 tstExtent->y0 != actualExtent->y0 ||
+                 tstExtent->y1 != actualExtent->y1) {
+                diag("ERROR: pixels set incorrectly for chip %d", chipID);
+                errorFlag = true;
+	    }
+
+            // Free temp memory
+            psFree(tstExtent);
+            psFree(actualExtent);
+	}
+        ok(!errorFlag, "pmChipPixels() passed all tests");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAPixels() tests: NULL input
+    // psRegion *pmFPAPixels(const pmFPA *fpa)
+    {
+        psMemId id = psMemGetId();
+        ok(NULL == pmFPAPixels(NULL), "pmFPAPixels(NULL) returned NULL");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAPixels() tests: acceptable inputs
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+
+        psRegion *actualExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of fpa
+        for (int chipID = 0 ; chipID < fpa->chips->n ; chipID++) {
+            pmChip *chip = fpa->chips->data[chipID];
+            psRegion *chipExtent = pmChipExtent(chip); // Extent of chip
+            actualExtent->x0 = PS_MIN(actualExtent->x0, chipExtent->x0);
+            actualExtent->x1 = PS_MAX(actualExtent->x1, chipExtent->x1);
+            actualExtent->y0 = PS_MIN(actualExtent->y0, chipExtent->y0);
+            actualExtent->y1 = PS_MAX(actualExtent->y1, chipExtent->y1);
+            psFree(chipExtent);
+        }
+
+        bool errorFlag = false;
+        psRegion *tstExtent = pmFPAPixels(fpa);
+        if (!tstExtent ||
+             tstExtent->x0 != actualExtent->x0 ||
+             tstExtent->x1 != actualExtent->x1 ||
+             tstExtent->y0 != actualExtent->y0 ||
+             tstExtent->y1 != actualExtent->y1) {
+            diag("ERROR: pmFPAPixels() set the pixels incorrectly");
+            errorFlag = true;
+	}
+        ok(!errorFlag, "pmFPAPixels() set the pixels psRegion correctly");
+
+        psFree(tstExtent);
+        psFree(actualExtent);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+
+}
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAFlags.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAFlags.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAFlags.c	(revision 15358)
@@ -0,0 +1,813 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+// XXX: Use better name for the temporary FITS file
+// XXX: The code to generate and free the FPA hierarchy was copied from
+// tap-pmFPA.c.  EIther include it directly, or library, or something.
+// Also, get rid of the manual free functions and use psFree() once
+// it correctly frees child members
+// XXX: For the genSimpleFPA() code, add IDs to each function so that
+// the values set in each chip-?cell-?hdu-?image are unique
+// XXX: For the genSimpleFPA() code, write masks and weights as well
+// XXX: Add in the associated CheckStatus tests
+
+#define CHIP_ALLOC_NAME        "ChipName"
+#define CELL_ALLOC_NAME        "CellName"
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define MISC_NAME2             "META01"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           4
+#define TEST_NUM_COLS           4
+#define NUM_READOUTS            3
+#define NUM_CELLS               10
+#define NUM_CHIPS               8
+#define NUM_HDUS                5
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    chip->hdu = pmHDUAlloc("chipExtName");
+//
+//    bool rc = pmConfigFileRead(&chip->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleChip())");
+//    }
+//
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip);
+    }
+
+    // XXX: Add code to initialize chip pmConcepts
+
+
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    fpa->hdu = pmHDUAlloc("fpaExtName");
+//
+//    bool rc = pmConfigFileRead(&fpa->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+//    }
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa);
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+//    bool mdok;
+//    psMetadata *fileData = psMetadataLookupMetadata(&mdok, fpa->hdu->format, "FILE");
+//    char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+//    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, NULL, fpaNameHdr);
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(72);
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmCellSetFileStatus() tests: verify with NULL pmCell param
+    // bool pmCellSetFileStatus(pmCell *cell, bool status)
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmCellSetFileStatus(NULL, false);
+        ok(!rc, "pmCellSetFileStatus() returned FALSE with NULL pmCell param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmCellSetFileStatus() tests: verify with acceptable data
+    // bool pmCellSetFileStatus(pmCell *cell, bool status)
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        // First, set all flags to FALSE
+        bool correctStatus = false;
+        bool rc = pmCellSetFileStatus(cell, correctStatus);
+        ok(rc, "pmCellSetFileStatus() returned successfully with acceptable input params");
+        bool errorFlag = false;
+        if (cell->file_exists != correctStatus) {
+            diag("TEST ERROR: pmCellSetFileStatus() failed to set file status for cell param\n");
+            errorFlag = true;
+        }
+        for (int i = 0; i < cell->readouts->n; i++) {
+
+            pmReadout *readout = cell->readouts->data[i];
+            if (readout->file_exists != correctStatus) {
+                diag("TEST ERROR: pmCellSetFileStatus() failed to set file status for cell %d\n", i);
+                errorFlag = true;
+            }
+        }
+        ok(!errorFlag, "pmCellSetFileStatus() set file status in all cells to FALSE");
+
+        // Second, set all flags to TRUE
+        correctStatus = true;
+        rc = pmCellSetFileStatus(cell, correctStatus);
+        ok(rc, "pmCellSetFileStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        if (cell->file_exists != correctStatus) {
+            diag("TEST ERROR: pmCellSetFileStatus() failed to set file status for cell param\n");
+            errorFlag = true;
+        }
+        for (int i = 0; i < cell->readouts->n; i++) {
+            pmReadout *readout = cell->readouts->data[i];
+            if (readout->file_exists != correctStatus) {
+                diag("TEST ERROR: pmCellSetFileStatus() failed to set file status for cell %d\n", i);
+                errorFlag = true;
+            }
+        }
+        ok(!errorFlag, "pmCellSetFileStatus() set file status in all cells to TRUE");
+
+        // Third, set all flags to FALSE
+        correctStatus = false;
+        rc = pmCellSetFileStatus(cell, correctStatus);
+        ok(rc, "pmCellSetFileStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        if (cell->file_exists != correctStatus) {
+            diag("TEST ERROR: pmCellSetFileStatus() failed to set file status for cell param\n");
+            errorFlag = true;
+        }
+        for (int i = 0; i < cell->readouts->n; i++) {
+            pmReadout *readout = cell->readouts->data[i];
+            if (readout->file_exists != correctStatus) {
+                diag("TEST ERROR: pmCellSetFileStatus() failed to set file status for readout %d\n", i);
+                errorFlag = true;
+            }
+        }
+        ok(!errorFlag, "pmCellSetFileStatus() set file status in all cells to FALSE");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmChipSetFileStatus() tests: verify with NULL pmChip param
+    // bool pmChipSetFileStatus(pmChip *chip, bool status)
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmChipSetFileStatus(NULL, false);
+        ok(!rc, "pmChipSetFileStatus() returned FALSE with NULL pmChip param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmChipSetFileStatus() tests: verify with acceptable data
+    // bool pmChipSetFileStatus(pmChip *chip, bool status)
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        // First, set all flags to FALSE
+        bool correctStatus = false;
+        bool rc = pmChipSetFileStatus(chip, correctStatus);
+        ok(rc, "pmChipSetFileStatus() returned successfully with acceptable input params");
+        bool errorFlag = false;
+        if (chip->file_exists != correctStatus) {
+            diag("TEST ERROR: pmChipSetFileStatus() failed to set file status for chip param");
+            errorFlag = true;
+        }
+        for (int j = 0 ; j < chip->cells->n ; j++) {
+            pmCell *cell = chip->cells->data[j];
+            if (cell->file_exists != correctStatus) {
+                diag("TEST ERROR: pmChipSetFileStatus() failed to set file status for cell %d\n", j);
+                errorFlag = true;
+            }
+
+            for (int i = 0; i < cell->readouts->n; i++) {
+                pmReadout *readout = cell->readouts->data[i];
+                if (readout->file_exists != correctStatus) {
+                    diag("TEST ERROR: pmChipSetFileStatus() failed to set file status for cell %d readout %d\n", j, i);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmChipSetFileStatus() set file status in all cells to FALSE");
+
+        // Second, set all flags to TRUE
+        correctStatus = true;
+        rc = pmChipSetFileStatus(chip, correctStatus);
+        ok(rc, "pmChipSetFileStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        if (chip->file_exists != correctStatus) {
+            diag("TEST ERROR: pmChipSetFileStatus() failed to set file status for chip param");
+            errorFlag = true;
+        }
+        for (int j = 0 ; j < chip->cells->n ; j++) {
+            pmCell *cell = chip->cells->data[j];
+            if (cell->file_exists != correctStatus) {
+                diag("TEST ERROR: pmChipSetFileStatus() failed to set file status for cell %d\n", j);
+                errorFlag = true;
+            }
+
+            for (int i = 0; i < cell->readouts->n; i++) {
+                pmReadout *readout = cell->readouts->data[i];
+                if (readout->file_exists != correctStatus) {
+                    diag("TEST ERROR: pmChipSetFileStatus() failed to set file status for cell %d readout %d\n", j, i);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmChipSetFileStatus() set file status in all cells to TRUE");
+
+        // Third, set all flags to FALSE
+        correctStatus = false;
+        rc = pmChipSetFileStatus(chip, correctStatus);
+        ok(rc, "pmChipSetFileStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        if (chip->file_exists != correctStatus) {
+            diag("TEST ERROR: pmChipSetFileStatus() failed to set file status for chip param");
+            errorFlag = true;
+        }
+        for (int j = 0 ; j < chip->cells->n ; j++) {
+            pmCell *cell = chip->cells->data[j];
+            if (cell->file_exists != correctStatus) {
+                diag("TEST ERROR: pmChipSetFileStatus() failed to set file status for cell %d\n", j);
+                errorFlag = true;
+            }
+
+            for (int i = 0; i < cell->readouts->n; i++) {
+                pmReadout *readout = cell->readouts->data[i];
+                if (readout->file_exists != correctStatus) {
+                    diag("TEST ERROR: pmChipSetFileStatus() failed to set file status for cell %d readout %d\n", j, i);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmChipSetFileStatus() set file status in all cells to FALSE");
+
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmFPASetFileStatus() tests: verify with NULL pmFPA param
+    // bool pmFPASetFileStatus(pmFPA *fpa, bool status)
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmFPASetFileStatus(NULL, false);
+        ok(!rc, "pmFPASetFileStatus() returned FALSE with NULL pmFPA param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPASetFileStatus() tests: verify with acceptable data
+    // bool pmFPASetFileStatus(pmFPA *fpa, bool status)
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        // First, set all flags to FALSE
+        bool correctStatus = false;
+        bool rc = pmFPASetFileStatus(fpa, correctStatus);
+        ok(rc, "pmFPASetFileStatus() returned successfully with acceptable input params");
+        bool errorFlag = false;
+        for (int k = 0 ; k < fpa->chips->n ; k++) {
+            pmChip *chip = fpa->chips->data[k];
+            for (int j = 0 ; j < chip->cells->n ; j++) {
+                pmCell *cell = chip->cells->data[j];
+                if (cell->file_exists != correctStatus) {
+                    diag("TEST ERROR: pmFPASetFileStatus() failed to set file status for chip %d cell %d\n", k, j);
+                    errorFlag = true;
+                }
+    
+                for (int i = 0; i < cell->readouts->n; i++) {
+                    pmReadout *readout = cell->readouts->data[i];
+                    if (readout->file_exists != correctStatus) {
+                        diag("TEST ERROR: pmFPASetFileStatus() failed to set file status for chip %d cell %d readout %d\n", k, j, i);
+                        errorFlag = true;
+                    }
+                }
+            }
+        }
+        ok(!errorFlag, "pmFPASetFileStatus() set file status in all cells to FALSE");
+
+        // Second, set all flags to TRUE
+        correctStatus = true;
+        rc = pmFPASetFileStatus(fpa, correctStatus);
+        ok(rc, "pmFPASetFileStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        for (int k = 0 ; k < fpa->chips->n ; k++) {
+            pmChip *chip = fpa->chips->data[k];
+            for (int j = 0 ; j < chip->cells->n ; j++) {
+                pmCell *cell = chip->cells->data[j];
+                if (cell->file_exists != correctStatus) {
+                    diag("TEST ERROR: pmFPASetFileStatus() failed to set file status for chip %d cell %d\n", k, j);
+                    errorFlag = true;
+                }
+    
+                for (int i = 0; i < cell->readouts->n; i++) {
+                    pmReadout *readout = cell->readouts->data[i];
+                    if (readout->file_exists != correctStatus) {
+                        diag("TEST ERROR: pmFPASetFileStatus() failed to set file status for chip %d cell %d readout %d\n", k, j, i);
+                        errorFlag = true;
+                    }
+                }
+            }
+        }
+        ok(!errorFlag, "pmFPASetFileStatus() set file status in all cells to TRUE");
+
+        // Third, set all flags to FALSE
+        correctStatus = false;
+        rc = pmFPASetFileStatus(fpa, correctStatus);
+        ok(rc, "pmFPASetFileStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        for (int k = 0 ; k < fpa->chips->n ; k++) {
+            pmChip *chip = fpa->chips->data[k];
+            for (int j = 0 ; j < chip->cells->n ; j++) {
+                pmCell *cell = chip->cells->data[j];
+                if (cell->file_exists != correctStatus) {
+                    diag("TEST ERROR: pmFPASetFileStatus() failed to set file status for chip %d cell %d\n", k, j);
+                    errorFlag = true;
+                }
+    
+                for (int i = 0; i < cell->readouts->n; i++) {
+                    pmReadout *readout = cell->readouts->data[i];
+                    if (readout->file_exists != correctStatus) {
+                        diag("TEST ERROR: pmFPASetFileStatus() failed to set file status for chip %d cell %d readout %d\n", k, j, i);
+                        errorFlag = true;
+                    }
+                }
+            }
+        }
+        ok(!errorFlag, "pmFPASetFileStatus() set file status in all cells to FALSE");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmCellSetDataStatus() tests: verify with NULL pmCell param
+    // bool pmCellSetDataStatus(pmCell *cell, bool status)
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmCellSetDataStatus(NULL, false);
+        ok(!rc, "pmCellSetDataStatus() returned FALSE with NULL pmCell param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmCellSetDataStatus() tests: verify with acceptable data
+    // bool pmCellSetDataStatus(pmCell *cell, bool status)
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        // First, set all flags to FALSE
+        bool correctStatus = false;
+        bool rc = pmCellSetDataStatus(cell, correctStatus);
+        ok(rc, "pmCellSetDataStatus() returned successfully with acceptable input params");
+        bool errorFlag = false;
+        if (cell->data_exists != correctStatus) {
+            diag("TEST ERROR: pmCellSetDataStatus() failed to set file status for cell param\n");
+            errorFlag = true;
+        }
+        for (int i = 0; i < cell->readouts->n; i++) {
+            pmReadout *readout = cell->readouts->data[i];
+            if (readout->data_exists != correctStatus) {
+                diag("TEST ERROR: pmCellSetDataStatus() failed to set file status for cell %d\n", i);
+                errorFlag = true;
+            }
+        }
+        ok(!errorFlag, "pmCellSetDataStatus() set data status in all cells to FALSE");
+
+        // Second, set all flags to TRUE
+        correctStatus = true;
+        rc = pmCellSetDataStatus(cell, correctStatus);
+        ok(rc, "pmCellSetDataStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        if (cell->data_exists != correctStatus) {
+            diag("TEST ERROR: pmCellSetDataStatus() failed to set file status for cell param\n");
+            errorFlag = true;
+        }
+        for (int i = 0; i < cell->readouts->n; i++) {
+            pmReadout *readout = cell->readouts->data[i];
+            if (readout->data_exists != correctStatus) {
+                diag("TEST ERROR: pmCellSetDataStatus() failed to set file status for cell %d\n", i);
+                errorFlag = true;
+            }
+        }
+        ok(!errorFlag, "pmCellSetDataStatus() set data status in all cells to TRUE");
+
+        // Third, set all flags to FALSE
+        correctStatus = false;
+        rc = pmCellSetDataStatus(cell, correctStatus);
+        ok(rc, "pmCellSetDataStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        if (cell->data_exists != correctStatus) {
+            diag("TEST ERROR: pmCellSetDataStatus() failed to set file status for cell param\n");
+            errorFlag = true;
+        }
+        for (int i = 0; i < cell->readouts->n; i++) {
+            pmReadout *readout = cell->readouts->data[i];
+            if (readout->data_exists != correctStatus) {
+                diag("TEST ERROR: pmCellSetDataStatus() failed to set file status for readout %d\n", i);
+                errorFlag = true;
+            }
+        }
+        ok(!errorFlag, "pmCellSetDataStatus() set data status in all cells to FALSE");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmChipSetDataStatus() tests: verify with NULL pmChip param
+    // bool pmChipSetDataStatus(pmChip *chip, bool status)
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmChipSetDataStatus(NULL, false);
+        ok(!rc, "pmChipSetDataStatus() returned FALSE with NULL pmChip param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmChipSetDataStatus() tests: verify with acceptable data
+    // bool pmChipSetDataStatus(pmChip *chip, bool status)
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        // First, set all flags to FALSE
+        bool correctStatus = false;
+        bool rc = pmChipSetDataStatus(chip, correctStatus);
+        ok(rc, "pmChipSetDataStatus() returned successfully with acceptable input params");
+        bool errorFlag = false;
+        if (chip->data_exists != correctStatus) {
+            diag("TEST ERROR (a): pmChipSetDataStatus() failed to set file status for chip param\n");
+            errorFlag = true;
+        }
+        for (int j = 0 ; j < chip->cells->n ; j++) {
+            pmCell *cell = chip->cells->data[j];
+            if (cell->data_exists != correctStatus) {
+                diag("TEST ERROR (b): pmChipSetDataStatus() failed to set file status for cell %d\n", j);
+                errorFlag = true;
+            }
+
+            for (int i = 0; i < cell->readouts->n; i++) {
+                pmReadout *readout = cell->readouts->data[i];
+                if (readout->data_exists != correctStatus) {
+                    diag("TEST ERROR (c): pmChipSetDataStatus() failed to set file status for cell %d readout %d\n", j, i);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmChipSetDataStatus() set data status in all cells to FALSE");
+
+        // Second, set all flags to TRUE
+        correctStatus = true;
+        rc = pmChipSetDataStatus(chip, correctStatus);
+        ok(rc, "pmChipSetDataStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        if (chip->data_exists != correctStatus) {
+            diag("TEST ERROR (a): pmChipSetDataStatus() failed to set file status for chip param\n");
+            errorFlag = true;
+        }
+        for (int j = 0 ; j < chip->cells->n ; j++) {
+            pmCell *cell = chip->cells->data[j];
+            if (cell->data_exists != correctStatus) {
+                diag("TEST ERROR (b): pmChipSetDataStatus() failed to set file status for cell %d\n", j);
+                errorFlag = true;
+            }
+
+            for (int i = 0; i < cell->readouts->n; i++) {
+                pmReadout *readout = cell->readouts->data[i];
+                if (readout->data_exists != correctStatus) {
+                    diag("TEST ERROR (c): pmChipSetDataStatus() failed to set file status for cell %d readout %d\n", j, i);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmChipSetDataStatus() set data status in all cells to TRUE");
+
+        // ThirdSecond, set all flags to FALSE
+        correctStatus = false;
+        rc = pmChipSetDataStatus(chip, correctStatus);
+        ok(rc, "pmChipSetDataStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        if (chip->data_exists != correctStatus) {
+            diag("TEST ERROR (a): pmChipSetDataStatus() failed to set file status for chip param\n");
+            errorFlag = true;
+        }
+        for (int j = 0 ; j < chip->cells->n ; j++) {
+            pmCell *cell = chip->cells->data[j];
+            if (cell->data_exists != correctStatus) {
+                diag("TEST ERROR (b): pmChipSetDataStatus() failed to set file status for cell %d\n", j);
+                errorFlag = true;
+            }
+
+            for (int i = 0; i < cell->readouts->n; i++) {
+                pmReadout *readout = cell->readouts->data[i];
+                if (readout->data_exists != correctStatus) {
+                    diag("TEST ERROR (c): pmChipSetDataStatus() failed to set file status for cell %d readout %d\n", j, i);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmChipSetDataStatus() set data status in all cells to FALSE");
+
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmFPASetFileStatus() tests: verify with NULL pmFPA param
+    // bool pmFPASetFileStatus(pmFPA *fpa, bool status)
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmFPASetFileStatus(NULL, false);
+        ok(!rc, "pmFPASetFileStatus() returned FALSE with NULL pmFPA param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+
+    // pmFPASetDataStatus() tests: verify with acceptable data
+    // bool pmFPASetDataStatus(pmFPA *fpa, bool status)
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        // First, set all flags to FALSE
+        bool correctStatus = false;
+        bool rc = pmFPASetDataStatus(fpa, correctStatus);
+        ok(rc, "pmFPASetDataStatus() returned successfully with acceptable input params");
+        bool errorFlag = false;
+        for (int k = 0 ; k < fpa->chips->n ; k++) {
+            pmChip *chip = fpa->chips->data[k];
+            for (int j = 0 ; j < chip->cells->n ; j++) {
+                pmCell *cell = chip->cells->data[j];
+                if (cell->data_exists != correctStatus) {
+                    diag("TEST ERROR: pmFPASetDataStatus() failed to set file status for chip %d cell %d\n", k, j);
+                    errorFlag = true;
+                }
+    
+                for (int i = 0; i < cell->readouts->n; i++) {
+                    pmReadout *readout = cell->readouts->data[i];
+                    if (readout->data_exists != correctStatus) {
+                        diag("TEST ERROR: pmFPASetDataStatus() failed to set file status for chip %d cell %d readout %d\n", k, j, i);
+                        errorFlag = true;
+                    }
+                }
+            }
+        }
+        ok(!errorFlag, "pmFPASetDataStatus() set file status in all cells to FALSE");
+
+        // Second, set all flags to TRUE
+        correctStatus = true;
+        rc = pmFPASetDataStatus(fpa, correctStatus);
+        ok(rc, "pmFPASetDataStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        for (int k = 0 ; k < fpa->chips->n ; k++) {
+            pmChip *chip = fpa->chips->data[k];
+            for (int j = 0 ; j < chip->cells->n ; j++) {
+                pmCell *cell = chip->cells->data[j];
+                if (cell->data_exists != correctStatus) {
+                    diag("TEST ERROR: pmFPASetDataStatus() failed to set file status for chip %d cell %d\n", k, j);
+                    errorFlag = true;
+                }
+    
+                for (int i = 0; i < cell->readouts->n; i++) {
+                    pmReadout *readout = cell->readouts->data[i];
+                    if (readout->data_exists != correctStatus) {
+                        diag("TEST ERROR: pmFPASetDataStatus() failed to set file status for chip %d cell %d readout %d\n", k, j, i);
+                        errorFlag = true;
+                    }
+                }
+            }
+        }
+        ok(!errorFlag, "pmFPASetDataStatus() set file status in all cells to TRUE");
+
+        // Third, set all flags to FALSE
+        correctStatus = false;
+        rc = pmFPASetDataStatus(fpa, correctStatus);
+        ok(rc, "pmFPASetDataStatus() returned successfully with acceptable input params");
+        errorFlag = false;
+        for (int k = 0 ; k < fpa->chips->n ; k++) {
+            pmChip *chip = fpa->chips->data[k];
+            for (int j = 0 ; j < chip->cells->n ; j++) {
+                pmCell *cell = chip->cells->data[j];
+                if (cell->data_exists != correctStatus) {
+                    diag("TEST ERROR: pmFPASetDataStatus() failed to set file status for chip %d cell %d\n", k, j);
+                    errorFlag = true;
+                }
+    
+                for (int i = 0; i < cell->readouts->n; i++) {
+                    pmReadout *readout = cell->readouts->data[i];
+                    if (readout->data_exists != correctStatus) {
+                        diag("TEST ERROR: pmFPASetDataStatus() failed to set file status for chip %d cell %d readout %d\n", k, j, i);
+                        errorFlag = true;
+                    }
+                }
+            }
+        }
+        ok(!errorFlag, "pmFPASetDataStatus() set file status in all cells to FALSE");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+}
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAHeader.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAHeader.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAHeader.c	(revision 15358)
@@ -0,0 +1,488 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+// XXX: Use better name for the temporary FITS file
+// XXX: The code to generate and free the FPA hierarchy was copied from
+// tap-pmFPA.c.  EIther include it directly, or library, or something.
+// Also, get rid of the manual free functions and use psFree() once
+// it correctly frees child members
+// XXX: For the genSimpleFPA() code, add IDs to each function so that
+// the values set in each chip-?cell-?hdu-?image are unique
+// XXX: For the genSimpleFPA() code, write masks and weights as well
+
+#define CHIP_ALLOC_NAME        "ChipName"
+#define CELL_ALLOC_NAME        "CellName"
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define MISC_NAME2             "META01"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           4
+#define TEST_NUM_COLS           4
+#define NUM_READOUTS            3
+#define NUM_CELLS               10
+#define NUM_CHIPS               8
+#define NUM_HDUS                5
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+char *fitsFilename = "tmp.fits";
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip, int cellID)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    char extname[80];
+    snprintf(extname,80, "ext-%d", cellID);
+    cell->hdu = pmHDUAlloc(extname);
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa, int chipID)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+
+    if (0) {
+        char extname[80];
+        snprintf(extname,80, "ext-%d", chipID);
+        chip->hdu = pmHDUAlloc(extname);
+    }
+
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip, i);
+    }
+
+    // XXX: Add code to initialize chip pmConcepts
+
+
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    fpa->hdu = pmHDUAlloc("fpaExtName");
+//
+//    bool rc = pmConfigFileRead(&fpa->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+//    }
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa, i);
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+//    bool mdok;
+//    psMetadata *fileData = psMetadataLookupMetadata(&mdok, fpa->hdu->format, "FILE");
+//    char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+//    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, NULL, fpaNameHdr);
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(25);
+
+
+    // ----------------------------------------------------------------------
+    // pmCellReadHeader() tests: NULL input fits file
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(!pmCellReadHeader(cell, NULL), "pmCellReadHeader(cell, NULL) returned FALSE");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmCellReadHeader() tests: NULL input pmCell
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(fitsFilename, "w");
+        ok(fitsFileW != NULL, "psFitsOpen() opened the FITS file");
+        ok(!pmCellReadHeader(NULL, fitsFileW), "pmCellReadHeader(NULL, fitsFile) returned FALSE");
+        psFree(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmCellReadHeader() tests: acceptable data
+    {
+        psMemId id = psMemGetId();
+
+        // Create a FITS file for this test
+        psFits* fitsFileW = psFitsOpen(fitsFilename, "w");
+        ok(fitsFileW != NULL, "psFitsOpen() opened the FITS file");
+        char extname[80];
+        for (int lcv = 0; lcv < NUM_HDUS; lcv++) {
+            snprintf(extname, 80, "ext-%d", lcv);
+            pmHDU *hdu = pmHDUAlloc(extname);
+            hdu->header = psMetadataAlloc();
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYINT", PS_DATA_S32,
+                         "psS32 Item", (psS32)lcv);
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYFLT", PS_DATA_F32,
+                         "psF32 Item", (float)(1.0f/(float)(1+lcv)));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYDBL", PS_DATA_F64,
+                         "psF64 Item", (double)(1.0/(double)(1+lcv)));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYBOOL", PS_DATA_BOOL,
+                         "psBool Item", (lcv%2 == 0));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYSTR", PS_DATA_STRING,
+                         "String Item", extname);
+            bool rc = pmConfigFileRead(&hdu->format, "data/camera0/format0.config", "Camera 0 Config Format");
+            ok(rc == true, "pmConfigFileRead() was successful");
+            rc = pmHDUWrite(hdu, fitsFileW);
+            ok(rc == true, "pmHDUWrite() successfully wrote the header");
+            psFree(hdu);
+        }
+        psFitsClose(fitsFileW);
+
+        // Now, open that FITS file, and create an pmFPA hierarchy
+        psFits* fitsFileR = psFitsOpen(fitsFilename, "r");
+        ok(fitsFileR != NULL, "psFitsOpen returned non-NULL on existing file");
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        ok(pmCellReadHeader(cell, fitsFileR), "pmCellReadHeader() returned TRUE with acceptable data");
+
+        // XXX: It's not clear if we should test if the HDU and pmConcepts actually
+        // get rid, since pmCellReadHeader() simply calls functions that are tested
+        // elsewhere.  However, if we should test it, test it here.
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmChipReadHeader() tests: NULL input fits file
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(!pmChipReadHeader(chip, NULL), "pmChipReadHeader(chip, NULL) returned FALSE");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmChipReadHeader() tests: NULL input pmCell
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(fitsFilename, "w");
+        ok(fitsFileW != NULL, "psFitsOpen() opened the FITS file");
+        ok(!pmChipReadHeader(NULL, fitsFileW), "pmChipReadHeader(NULL, fitsFile) returned FALSE");
+        psFree(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmChipReadHeader() tests: acceptable data
+    {
+        psMemId id = psMemGetId();
+
+        // Create a FITS file for this test
+        psFits* fitsFileW = psFitsOpen(fitsFilename, "w");
+        ok(fitsFileW != NULL, "psFitsOpen() opened the FITS file");
+        char extname[80];
+        for (int lcv = 0; lcv < NUM_HDUS; lcv++) {
+            snprintf(extname, 80, "ext-%d", lcv);
+            pmHDU *hdu = pmHDUAlloc(extname);
+            hdu->header = psMetadataAlloc();
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYINT", PS_DATA_S32,
+                         "psS32 Item", (psS32)lcv);
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYFLT", PS_DATA_F32,
+                         "psF32 Item", (float)(1.0f/(float)(1+lcv)));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYDBL", PS_DATA_F64,
+                         "psF64 Item", (double)(1.0/(double)(1+lcv)));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYBOOL", PS_DATA_BOOL,
+                         "psBool Item", (lcv%2 == 0));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYSTR", PS_DATA_STRING,
+                         "String Item", extname);
+            bool rc = pmConfigFileRead(&hdu->format, "data/camera0/format0.config", "Camera 0 Config Format");
+            ok(rc == true, "pmConfigFileRead() was successful");
+            rc = pmHDUWrite(hdu, fitsFileW);
+            ok(rc == true, "pmHDUWrite() successfully wrote the header");
+            psFree(hdu);
+        }
+        psFitsClose(fitsFileW);
+
+        // Now, open that FITS file, and create an pmFPA hierarchy
+        psFits* fitsFileR = psFitsOpen(fitsFilename, "r");
+        ok(fitsFileR != NULL, "psFitsOpen returned non-NULL on existing file");
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        // Set the correct extension names for the chips (if we put this in the
+        // generateChip() code, then psMOdules aborts.
+        for (int chipID = 0 ; chipID < fpa->chips->n ; chipID++) {
+            pmChip *chip = fpa->chips->data[chipID];
+            char extname[80];
+            snprintf(extname,80, "ext-%d", chipID);
+            chip->hdu = pmHDUAlloc(extname);
+	}
+
+        ok(pmChipReadHeader(chip, fitsFileR), "pmChipReadHeader() returned TRUE with acceptable data");
+
+
+        // XXX: It's not clear if we should test if the HDU and pmConcepts actually
+        // get rid, since pmCellReadHeader() simply calls functions that are tested
+        // elsewhere.  However, if we should test it, test it here.
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAReadHeader() tests: NULL input fits file
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(!pmFPAReadHeader(fpa, NULL), "pmFPAReadHeader(fpa, NULL) returned FALSE");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAReadHeader() tests: NULL input pmCell
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(fitsFilename, "w");
+        ok(fitsFileW != NULL, "psFitsOpen() opened the FITS file");
+        ok(!pmFPAReadHeader(NULL, fitsFileW), "pmFPAReadHeader(NULL, fitsFile) returned FALSE");
+        psFree(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAReadHeader() tests: acceptable data
+    {
+        psMemId id = psMemGetId();
+
+        // Create a FITS file for this test
+        psFits* fitsFileW = psFitsOpen(fitsFilename, "w");
+        ok(fitsFileW != NULL, "psFitsOpen() opened the FITS file");
+        char extname[80];
+        for (int lcv = 0; lcv < NUM_HDUS; lcv++) {
+            snprintf(extname, 80, "ext-%d", lcv);
+            pmHDU *hdu = pmHDUAlloc(extname);
+            hdu->header = psMetadataAlloc();
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYINT", PS_DATA_S32,
+                         "psS32 Item", (psS32)lcv);
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYFLT", PS_DATA_F32,
+                         "psF32 Item", (float)(1.0f/(float)(1+lcv)));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYDBL", PS_DATA_F64,
+                         "psF64 Item", (double)(1.0/(double)(1+lcv)));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYBOOL", PS_DATA_BOOL,
+                         "psBool Item", (lcv%2 == 0));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYSTR", PS_DATA_STRING,
+                         "String Item", extname);
+            bool rc = pmConfigFileRead(&hdu->format, "data/camera0/format0.config", "Camera 0 Config Format");
+            ok(rc == true, "pmConfigFileRead() was successful");
+            rc = pmHDUWrite(hdu, fitsFileW);
+            ok(rc == true, "pmHDUWrite() successfully wrote the header");
+            psFree(hdu);
+        }
+        psFitsClose(fitsFileW);
+
+        // Now, open that FITS file, and create an pmFPA hierarchy
+        psFits* fitsFileR = psFitsOpen(fitsFilename, "r");
+        ok(fitsFileR != NULL, "psFitsOpen returned non-NULL on existing file");
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        // Set the correct extension names for the chips (if we put this in the
+        // generateChip() code, then psMOdules aborts.
+        for (int chipID = 0 ; chipID < fpa->chips->n ; chipID++) {
+            pmChip *chip = fpa->chips->data[chipID];
+            char extname[80];
+            snprintf(extname,80, "ext-%d", chipID);
+            chip->hdu = pmHDUAlloc(extname);
+	}
+        snprintf(extname,80, "ext-%d", 0);
+        fpa->hdu = pmHDUAlloc(extname);
+
+        ok(pmFPAReadHeader(fpa, fitsFileR), "pmFPAReadHeader() returned TRUE with acceptable data");
+
+        // XXX: It's not clear if we should test if the HDU and pmConcepts actually
+        // get rid, since pmCellReadHeader() simply calls functions that are tested
+        // elsewhere.  However, if we should test it, test it here.
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+
+}
+
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPALevel.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPALevel.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPALevel.c	(revision 15358)
@@ -0,0 +1,76 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+#define	ERR_TRACE_LEVEL		0
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(15);
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmFPALevelToName(): tests
+    // const char *pmFPALevelToName(pmFPALevel level)
+    {
+        psMemId id = psMemGetId();
+        char *str = (char *) pmFPALevelToName(PM_FPA_LEVEL_NONE);
+        ok(!strcmp(str, "NONE"), "pmFPALevelToName(PM_FPA_LEVEL_NONE)");
+
+        str = (char *) pmFPALevelToName(PM_FPA_LEVEL_FPA);
+        ok(!strcmp(str, "FPA"), "pmFPALevelToName(PM_FPA_LEVEL_FPA)");
+
+        str = (char *) pmFPALevelToName(PM_FPA_LEVEL_CHIP);
+        ok(!strcmp(str, "CHIP"), "pmFPALevelToName(PM_FPA_LEVEL_CHIP)");
+
+        str = (char *) pmFPALevelToName(PM_FPA_LEVEL_CELL);
+        ok(!strcmp(str, "CELL"), "pmFPALevelToName(PM_FPA_LEVEL_CELL)");
+
+        str = (char *) pmFPALevelToName(PM_FPA_LEVEL_READOUT);
+        ok(!strcmp(str, "READOUT"), "pmFPALevelToName(PM_FPA_LEVEL_READOUT)");
+
+        // XXX: We avoid this because pmFPALevelToName() aborts
+        if (0) {
+            str = (char *) pmFPALevelToName(-1);
+            ok(str == NULL, "pmFPALevelToName(-1)");
+	}
+
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmFPALevelFromName(): tests
+    // pmFPALevel pmFPALevelFromName(const char *name)
+    {
+        psMemId id = psMemGetId();
+        pmFPALevel lev = pmFPALevelFromName("NONE");
+        ok(lev == PM_FPA_LEVEL_NONE, "pmFPALevelToName(NONE)");
+
+        lev = pmFPALevelFromName("FPA");
+        ok(lev == PM_FPA_LEVEL_FPA, "pmFPALevelToName(FPA)");
+
+        lev = pmFPALevelFromName("CHIP");
+        ok(lev == PM_FPA_LEVEL_CHIP, "pmFPALevelToName(CHIP)");
+
+        lev = pmFPALevelFromName("CELL");
+        ok(lev == PM_FPA_LEVEL_CELL, "pmFPALevelToName(CELL)");
+
+        lev = pmFPALevelFromName("READOUT");
+        ok(lev == PM_FPA_LEVEL_READOUT, "pmFPALevelToName(READOUT)");
+
+        lev = pmFPALevelFromName("BOGUS");
+        ok(lev == PM_FPA_LEVEL_NONE, "pmFPALevelToName(BOGUS)");
+
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAMaskW.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAMaskW.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAMaskW.c	(revision 15358)
@@ -0,0 +1,477 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+// XXX: Use better name for the temporary FITS file
+// XXX: The code to generate and free the FPA hierarchy was copied from
+// tap-pmFPA.c.  EIther include it directly, or library, or something.
+// Also, get rid of the manual free functions and use psFree() once
+// it correctly frees child members
+// XXX: For the genSimpleFPA() code, add IDs to each function so that
+// the values set in each chip-?cell-?hdu-?image are unique
+// XXX: For the genSimpleFPA() code, write masks and weights as well
+// XXX: Must add tests for pmReadoutGenerateWeight()
+// XXX: We don't test pmReadoutGenerateMaskWeight() and pmCellGenerateMaskWeight()
+// because they are simply calls to the above tested functions
+
+#define CHIP_ALLOC_NAME        "ChipName"
+#define CELL_ALLOC_NAME        "CellName"
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define MISC_NAME2             "META01"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           4
+#define TEST_NUM_COLS           4
+#define NUM_READOUTS            3
+#define NUM_CELLS               10
+#define NUM_CHIPS               8
+#define NUM_HDUS                5
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+#define SATURATION_LEVEL	10000.0
+#define BAD_LEVEL		100.0
+#define SATURATION_MASK		1
+#define BAD_MASK		2
+#define CELL_GAIN		1.0
+#define CELL_READNOISE		2.0
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (int i = 0 ; i < TEST_NUM_ROWS ; i++) {
+        for (int j = 0 ; j < TEST_NUM_COLS ; j++) {
+            readout->image->data.F32[i][j] = 32.0;
+            readout->mask->data.U8[i][j] = 0;
+            readout->weight->data.F32[i][j] = 1.0;
+	}
+    }
+
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    chip->hdu = pmHDUAlloc("chipExtName");
+//
+//    bool rc = pmConfigFileRead(&chip->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleChip())");
+//    }
+//
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip);
+    }
+
+    // XXX: Add code to initialize chip pmConcepts
+
+
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    fpa->hdu = pmHDUAlloc("fpaExtName");
+//
+//    bool rc = pmConfigFileRead(&fpa->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+//    }
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa);
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+//    bool mdok;
+//    psMetadata *fileData = psMetadataLookupMetadata(&mdok, fpa->hdu->format, "FILE");
+//    char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+//    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, NULL, fpaNameHdr);
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(18);
+
+    // ----------------------------------------------------------------------
+    // pmReadoutSetMask() tests: NULL inputs
+    // bool pmReadoutSetMask(pmReadout *readout, psMaskType satMask, psMaskType badMask)
+    if (1) {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        pmReadout *readout = cell->readouts->data[0];
+        bool rc;
+
+        // Set readout == NULL, ensure pmReadoutSetMask() returnes FALSE, with no seg faults, memory leaks
+        rc = pmReadoutSetMask(NULL, SATURATION_MASK, BAD_MASK);
+        ok(!rc, "pmReadoutSetMask(NULL, SATURATION_MASK, BAD_MASK) returned FALSE with null pmReadout input");
+
+        // Set readout->image, ensure pmReadoutSetMask() returnes FALSE, with no seg faults, memory leaks
+        psImage *saveImg = readout->image;
+        readout->image = NULL;
+        rc = pmReadoutSetMask(readout, SATURATION_MASK, BAD_MASK);
+        ok(!rc, "pmReadoutSetMask(readout, SATURATION_MASK, BAD_MASK) returned FALSE with null pmReadout->image input");
+        readout->image = saveImg;
+
+        // Set pixels in the upper-left quadrant to values [10000:11000] range
+        // Set pixels in the upper-right quadrant to values [0:1000] range
+        // Set pixels in the lower-left quadrant to values [100000:1000000] range
+        // Set pixels in the lower-right quadrant to values [100000:1000000] range
+
+        for (int i = 0 ; i < readout->image->numRows ; i++) {
+            for (int j = 0 ; j < readout->image->numCols ; j++) {
+                if (i < readout->image->numRows/2) {
+                    if (j < readout->image->numCols/2) {
+                        readout->image->data.F32[i][j] = 10000.0 + (float) (i + j);
+		    } else {
+                        readout->image->data.F32[i][j] = (float) (i + j);
+		    }
+		} else {
+                    readout->image->data.F32[i][j] = 100000.0 + (float) (i + j);
+		}
+	    }
+	}
+
+        // Set the acceptable pixel range to [100.0 : 20000.0]
+        rc = psMetadataAddF32(readout->parent->concepts, PS_LIST_HEAD, "CELL.SATURATION", PS_META_REPLACE, NULL, 20000.0);
+        rc|= psMetadataAddF32(readout->parent->concepts, PS_LIST_HEAD, "CELL.BAD", PS_META_REPLACE, NULL, 100.0);
+        ok(rc, "Set pixel range in cell->concepts successfully");
+
+        // Call pmReadoutSetMask() and then verify that the mask data was set correctly
+        rc = pmReadoutSetMask(readout, SATURATION_MASK, BAD_MASK);
+        ok(rc, "pmReadoutSetMask(readout, SATURATION_MASK, BAD_MASK) returned TRUE with acceptable input data");
+        bool errorFlag = false;
+        for (int i = 0 ; i < readout->image->numRows ; i++) {
+            for (int j = 0 ; j < readout->image->numCols ; j++) {
+                if (i < readout->image->numRows/2) {
+                    if (j < readout->image->numCols/2) {
+                        if(readout->mask->data.U8[i][j] != 0) {
+                            if (VERBOSE) {
+                                diag("TEST ERROR: mask[%d][%d] is %d, should be 0\n",
+                                      i, j, readout->mask->data.U8[i][j]);
+			    }
+                            errorFlag = true;
+			}
+		    } else {
+                        if(readout->mask->data.U8[i][j] != BAD_MASK) {
+                            if (VERBOSE) {
+                                diag("TEST ERROR: mask[%d][%d] is %d, should be %d\n",
+                                      i, j, readout->mask->data.U8[i][j], BAD_MASK);
+			    }
+                            errorFlag = true;
+			}
+		    }
+		} else {
+                    if(readout->mask->data.U8[i][j] != SATURATION_MASK) {
+                        if (VERBOSE) {
+                            diag("TEST ERROR: mask[%d][%d] is %d, should be %d\n",
+                                  i, j, readout->mask->data.U8[i][j], SATURATION_MASK);
+                        }
+                        errorFlag = true;
+		    }
+		}
+	    }
+	}
+        ok(!errorFlag, "pmReadoutSetMask() set the mask values correctly");
+        myFreeFPA(fpa);    
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmReadoutGenerateMask() tests: NULL inputs
+    // bool pmReadoutGenerateMask(pmReadout *readout, psMaskType satMask, psMaskType badMask)
+    // XXX: This test is a duplicate of the above pmReadoutSetMask() test since the actual
+    // code is almost the same.  Must test with the readout->mask == NULL.
+    if (1) {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        pmReadout *readout = cell->readouts->data[0];
+        bool rc;
+
+        // Set readout == NULL, ensure pmReadoutGenerateMask() returnes FALSE, with no seg faults, memory leaks
+        rc = pmReadoutGenerateMask(NULL, SATURATION_MASK, BAD_MASK);
+        ok(!rc, "pmReadoutGenerateMask(NULL, SATURATION_MASK, BAD_MASK) returned FALSE with null pmReadout input");
+
+        // Set pixels in the upper-left quadrant to values [10000:11000] range
+        // Set pixels in the upper-right quadrant to values [0:1000] range
+        // Set pixels in the lower-left quadrant to values [100000:1000000] range
+        // Set pixels in the lower-right quadrant to values [100000:1000000] range
+
+        for (int i = 0 ; i < readout->image->numRows ; i++) {
+            for (int j = 0 ; j < readout->image->numCols ; j++) {
+                if (i < readout->image->numRows/2) {
+                    if (j < readout->image->numCols/2) {
+                        readout->image->data.F32[i][j] = 10000.0 + (float) (i + j);
+		    } else {
+                        readout->image->data.F32[i][j] = (float) (i + j);
+		    }
+		} else {
+                    readout->image->data.F32[i][j] = 100000.0 + (float) (i + j);
+		}
+	    }
+	}
+
+        // Set the acceptable pixel range to [100.0 : 20000.0]
+        rc = psMetadataAddF32(readout->parent->concepts, PS_LIST_HEAD, "CELL.SATURATION", PS_META_REPLACE, NULL, 20000.0);
+        rc|= psMetadataAddF32(readout->parent->concepts, PS_LIST_HEAD, "CELL.BAD", PS_META_REPLACE, NULL, 100.0);
+        ok(rc, "Set pixel range in cell->concepts successfully");
+
+        // Call pmReadoutGenerateMask() and then verify that the mask data was set correctly
+        rc = pmReadoutGenerateMask(readout, SATURATION_MASK, BAD_MASK);
+        ok(rc, "pmReadoutGenerateMask(readout, SATURATION_MASK, BAD_MASK) returned TRUE with acceptable input data");
+        bool errorFlag = false;
+        for (int i = 0 ; i < readout->image->numRows ; i++) {
+            for (int j = 0 ; j < readout->image->numCols ; j++) {
+                if (i < readout->image->numRows/2) {
+                    if (j < readout->image->numCols/2) {
+                        if(readout->mask->data.U8[i][j] != 0) {
+                            if (VERBOSE) {
+                                diag("TEST ERROR: mask[%d][%d] is %d, should be 0\n",
+                                      i, j, readout->mask->data.U8[i][j]);
+			    }
+                            errorFlag = true;
+			}
+		    } else {
+                        if(readout->mask->data.U8[i][j] != BAD_MASK) {
+                            if (VERBOSE) {
+                                diag("TEST ERROR: mask[%d][%d] is %d, should be %d\n",
+                                      i, j, readout->mask->data.U8[i][j], BAD_MASK);
+                                errorFlag = true;
+			    }
+			}
+		    }
+		} else {
+                    if(readout->mask->data.U8[i][j] != SATURATION_MASK) {
+                        if (VERBOSE) {
+                            diag("TEST ERROR: mask[%d][%d] is %d, should be %d\n",
+                                  i, j, readout->mask->data.U8[i][j], SATURATION_MASK);
+                        }
+                        errorFlag = true;
+		    }
+		}
+	    }
+	}
+        ok(!errorFlag, "pmReadoutGenerateMask() set the mask values correctly");
+
+        myFreeFPA(fpa);    
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmReadoutSetWeight() tests: NULL inputs
+    // bool pmReadoutSetWeight(pmReadout *readout, bool poisson)
+    if (1) {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        pmReadout *readout = cell->readouts->data[0];
+        bool rc;
+
+        // Set readout == NULL, ensure pmReadoutSetWeight() returnes FALSE, with no seg faults, memory leaks
+        rc = pmReadoutSetWeight(NULL, false);
+        ok(!rc, "pmReadoutSetWeight(NULL, false) returned FALSE with null pmReadout input");
+
+
+        // Set the acceptable pixel range to [100.0 : 20000.0]
+        rc = psMetadataAddF32(readout->parent->concepts, PS_LIST_HEAD, "CELL.GAIN", PS_META_REPLACE, NULL, CELL_GAIN);
+        rc|= psMetadataAddF32(readout->parent->concepts, PS_LIST_HEAD, "CELL.READNOISE", PS_META_REPLACE, NULL, CELL_READNOISE);
+        ok(rc, "Set GAIN and READNOISE in cell->concepts successfully");
+
+        // Call pmReadoutSetWeight() and then verify that the mask data was set correctly
+        rc = pmReadoutSetWeight(readout, false);
+        ok(rc, "pmReadoutSetWeight(readout, false) returned TRUE with acceptable input data");
+        bool errorFlag = false;
+        for (int i = 0 ; i < readout->weight->numRows ; i++) {
+            for (int j = 0 ; j < readout->weight->numCols ; j++) {
+                psF32 exp = CELL_READNOISE * CELL_READNOISE / CELL_GAIN / CELL_GAIN;
+                if(abs(readout->weight->data.F32[i][j] - exp) > 1e-4) {
+                    if (VERBOSE) {
+                        diag("TEST ERROR: weight[%d][%d] is %.2f, should be %.2f\n",
+                              i, j, readout->weight->data.F32[i][j], exp);
+		    }
+                    errorFlag = true;
+		}
+	    }
+	}
+        ok(!errorFlag, "pmReadoutSetWeight() set the weight values correctly (non-Poisson)");
+
+        for (int i = 0 ; i < readout->image->numRows ; i++) {
+            for (int j = 0 ; j < readout->image->numCols ; j++) {
+               readout->image->data.F32[i][j] = 100.0 + (float) (i + j);
+	    }
+	}
+        // Call pmReadoutSetWeight() and then verify that the mask data was set correctly
+        rc = pmReadoutSetWeight(readout, true);
+        ok(rc, "pmReadoutSetWeight(readout, true) returned TRUE with acceptable input data");
+        errorFlag = false;
+        for (int i = 0 ; i < readout->weight->numRows ; i++) {
+            for (int j = 0 ; j < readout->weight->numCols ; j++) {
+                psF32 exp = abs(readout->image->data.F32[i][j] / CELL_GAIN); 
+                if (exp < 1.0) exp = 1.0;
+                exp+= CELL_READNOISE * CELL_READNOISE / CELL_GAIN / CELL_GAIN;
+                if(abs(readout->weight->data.F32[i][j] - exp) > 1e-4) {
+                    if (VERBOSE) {
+                        diag("TEST ERROR: weight[%d][%d] is %.2f, should be %.2f\n",
+                              i, j, readout->weight->data.F32[i][j], exp);
+		    }
+                    errorFlag = true;
+		}
+	    }
+	}
+
+        ok(!errorFlag, "pmReadoutSetWeight() set the weight values correctly (Poisson)");
+        myFreeFPA(fpa);    
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+}
+
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAReadWrite.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAReadWrite.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAReadWrite.c	(revision 15358)
@@ -0,0 +1,1256 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+// XXX: Use better name for the temporary FITS file
+// XXX: The code to generate and free the FPA hierarchy was copied from
+// tap-pmFPA.c.  EIther include it directly, or library, or something.
+// Also, get rid of the manual free functions and use psFree() once
+// it correctly frees child members
+// XXX: For the genSimpleFPA() code, add IDs to each function so that
+// the values set in each chip-?cell-?hdu-?image are unique
+
+#define CHIP_ALLOC_NAME		"ChipName"
+#define CELL_ALLOC_NAME		"CellName"
+#define MISC_NUM		32
+#define MISC_NAME		"META00"
+#define MISC_NAME2		"META01"
+#define NUM_BIAS_DATA		10
+#define TEST_NUM_ROWS		4
+#define TEST_NUM_COLS		4
+#define NUM_READOUTS		3
+#define NUM_CELLS		10
+#define NUM_CHIPS		8
+#define NUM_HDUS		5
+#define BASE_IMAGE		10
+#define BASE_MASK		40
+#define BASE_WEIGHT		70
+#define VERBOSE			0
+#define ERR_TRACE_LEVEL		0
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    chip->hdu = pmHDUAlloc("chipExtName");
+//
+//    bool rc = pmConfigFileRead(&chip->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleChip())");
+//    }
+//
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip);
+    }
+
+    // XXX: Add code to initialize chip pmConcepts
+
+
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    fpa->hdu = pmHDUAlloc("fpaExtName");
+//
+//    bool rc = pmConfigFileRead(&fpa->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+//    }
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa);
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+//    bool mdok;
+//    psMetadata *fileData = psMetadataLookupMetadata(&mdok, fpa->hdu->format, "FILE");
+//    char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+//    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, NULL, fpaNameHdr);
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(405);
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmCellWrite(): tests
+    // Verify pmCellWrite() with NULL pmCell arg
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp00", "w");
+        ok(!pmCellWrite(NULL, fitsFileW, NULL, false), "pmCellWrite() returned FALSE with NULL pmCell input");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Verify pmCellWrite() with NULL pmCell arg
+    // XXXX: Big problem: Without the next code, everything else fails.  Why?
+    if (1) {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp00", "w");
+        ok(!pmCellWrite(NULL, fitsFileW, NULL, false), "pmCellWrite() returned FALSE with NULL pmCell input");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellWrite() with NULL pmFits arg
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(!pmCellWrite(cell, NULL, NULL, false), "pmCellWrite() returned FALSE with NULL psFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellWrite() with acceptable input params
+    // We first write a FITS file with the pmCellWrite(), then we read it and verify.
+    // First call pmCellWrite()
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp00", "w");
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        //  Use pmCellWrite() to write image data to the FITS file
+        bool rc = pmCellWrite(cell, fitsFileW, NULL, false);
+        ok(rc, "pmCellWrite() returned TRUE");
+
+        //  Close the FITS file, free memory
+        psFitsClose(fitsFileW);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmCellRead() tests 
+    // Verify pmCellRead() with NULL pmCell param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(!pmCellRead(NULL, fitsFileR, NULL), "pmCellRead() returned FALSE with NULL pmCell param");
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellRead() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(!pmCellRead(cell, NULL, NULL), "pmCellRead() returned FALSE with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellRead() with acceptable data (using the FITS file created above)
+    {
+        psMemId id = psMemGetId();
+        bool rc;
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        // Free the existing cell hdu image data (so we can verify that pmCellRead() actually reads the data
+        psFree(cell->hdu->images);
+        cell->hdu->images = NULL;
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+
+        rc = pmCellRead(cell, fitsFileR, NULL);
+        ok(rc, "pmCellRead() returned TRUE");
+        for (int k = 0 ; k < cell->hdu->images->n ; k++) {
+            bool errorFlag = false;
+            psImage *img = cell->hdu->images->data[k];
+            for (int i = 0 ; i < img->numRows ; i++) {
+                for (int j = 0 ; j < img->numCols ; j++) {
+                    if (((float) (BASE_IMAGE+k)) != img->data.F32[i][j]) {
+                        diag("TEST ERROR: img[%d][%d] is %.2f, should be %.2f\n", i, j,
+                              img->data.F32[i][j], ((float) (BASE_IMAGE+k)));
+                        errorFlag = true;
+		    }
+		}
+	    }
+            ok(!errorFlag, "pmCellWrite()/pmCellRead() properly set the image data (image %d)", k);
+        }
+        psFitsClose(fitsFileR);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmCellWriteWeight(): tests
+    // Verify pmCellWriteWeight() with NULL pmCell arg
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp00", "w");
+        ok(!pmCellWriteWeight(NULL, fitsFileW, NULL, false), "pmCellWriteWeight() returned FALSE with NULL pmCell input");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellWriteWeight() with NULL pmFits arg
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(!pmCellWriteWeight(cell, NULL, NULL, false), "pmCellWriteWeight() returned FALSE with NULL psFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellWriteWeight() with acceptable input params
+    // We first write a FITS file with the pmCellWriteWeight(), then we read it and verify.
+    // First call pmCellWriteWeight()
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp00", "w");
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        //  Use pmCellWriteWeight() to write weight data to the FITS file
+        bool rc = pmCellWriteWeight(cell, fitsFileW, NULL, false);
+        ok(rc, "pmCellWriteWeight() returned TRUE");
+
+        //  Close the FITS file, free memory
+        psFitsClose(fitsFileW);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmCellReadWeight() tests 
+    // Verify pmCellReadWeight() with NULL pmCell param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(!pmCellReadWeight(NULL, fitsFileR, NULL), "pmCellReadWeight() returned FALSE with NULL pmCell param");
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellReadWeight() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(!pmCellReadWeight(cell, NULL, NULL), "pmCellReadWeight() returned FALSE with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellReadWeight() with acceptable data (using the FITS file created above)
+    {
+        psMemId id = psMemGetId();
+        bool rc;
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        // Free the existing cell hdu weight data (so we can verify that pmCellReadWeight() actually reads the data
+        psFree(cell->hdu->weights);
+        cell->hdu->weights = NULL;
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+
+        rc = pmCellReadWeight(cell, fitsFileR, NULL);
+        ok(rc, "pmCellReadWeight() returned TRUE");
+        for (int k = 0 ; k < cell->hdu->weights->n ; k++) {
+            bool errorFlag = false;
+            psImage *msk = cell->hdu->weights->data[k];
+            for (int i = 0 ; i < msk->numRows ; i++) {
+                for (int j = 0 ; j < msk->numCols ; j++) {
+                    if (((float) (BASE_WEIGHT+k)) != msk->data.F32[i][j]) {
+                        diag("TEST ERROR: msk[%d][%d] is %.2f, should be %.2f\n", i, j,
+                              msk->data.F32[i][j], ((float) (BASE_WEIGHT+k)));
+                        errorFlag = true;
+		    }
+		}
+	    }
+            ok(!errorFlag, "pmCellWriteWeight()/pmCellReadWeight() properly set the weight data (image %d)", k);
+        }
+        psFitsClose(fitsFileR);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmCellWriteMask(): tests
+    // Verify pmCellWriteMask() with NULL pmCell arg
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp00", "w");
+        ok(!pmCellWriteMask(NULL, fitsFileW, NULL, false), "pmCellWriteMask() returned FALSE with NULL pmCell input");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellWriteMask() with NULL pmFits arg
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(!pmCellWriteMask(cell, NULL, NULL, false), "pmCellWriteMask() returned FALSE with NULL psFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellWriteMask() with acceptable input params
+    // We first write a FITS file with the pmCellWriteMask(), then we read it and verify.
+    // First call pmCellWriteMask()
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp00", "w");
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(cell != NULL, "Allocated a pmCell successfully");
+
+        //  Use pmCellWriteMask() to write mask data to the FITS file
+        bool rc = pmCellWriteMask(cell, fitsFileW, NULL, false);
+        ok(rc, "pmCellWriteMask() returned TRUE");
+
+        //  Close the FITS file, free memory
+        psFitsClose(fitsFileW);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmCellReadMask() tests 
+    // Verify pmCellReadMask() with NULL pmCell param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(!pmCellReadMask(NULL, fitsFileR, NULL), "pmCellReadMask() returned FALSE with NULL pmCell param");
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellReadMask() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(!pmCellReadMask(cell, NULL, NULL), "pmCellReadMask() returned FALSE with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmCellReadMask() with acceptable data (using the FITS file created above)
+    {
+        psMemId id = psMemGetId();
+        bool rc;
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        // Free the existing cell hdu mask data (so we can verify that pmCellReadMask() actually reads the data
+        psFree(cell->hdu->masks);
+        cell->hdu->masks = NULL;
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+
+        rc = pmCellReadMask(cell, fitsFileR, NULL);
+        ok(rc, "pmCellReadMask() returned TRUE");
+        for (int k = 0 ; k < cell->hdu->masks->n ; k++) {
+            bool errorFlag = false;
+            psImage *msk = cell->hdu->masks->data[k];
+            for (int i = 0 ; i < msk->numRows ; i++) {
+                for (int j = 0 ; j < msk->numCols ; j++) {
+                    if (0) {
+                        if (((float) (BASE_WEIGHT+k)) != msk->data.F32[i][j]) {
+                            diag("TEST ERROR: msk[%d][%d] is %.2f, should be %.2f\n", i, j,
+                                  msk->data.F32[i][j], ((float) (BASE_WEIGHT+k)));
+                            errorFlag = true;
+                        }
+		    }
+                    if (1) {
+                        if (((BASE_MASK+k)) != msk->data.U8[i][j]) {
+                            diag("TEST ERROR: msk[%d][%d] is %.2f, should be %.2f\n", i, j,
+                                  msk->data.F32[i][j], ((float) (BASE_MASK+k)));
+                            errorFlag = true;
+                        }
+		    }
+    		}
+	    }
+            ok(!errorFlag, "pmCellWriteMask()/pmCellReadMask() properly set the mask data (image %d)", k);
+        }
+        psFitsClose(fitsFileR);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmChipWrite() tests
+    // Verify pmChipWrite() with NULL pmChip param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        ok(!pmChipWrite(NULL, fitsFileW, NULL, false, true), "pmChipWrite() returned NULL with NULL pmChip param");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipWrite() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(!pmChipWrite(chip, NULL, NULL, false, true), "pmChipWrite() returned NULL with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipWrite() with acceptable data
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        ok(chip != NULL, "Allocated a pmChip successfully");
+
+        //  Use pmChipWrite() to write image data to the FITS file
+        bool rc = pmChipWrite(chip, fitsFileW, NULL, false, true);
+        ok(rc, "pmChipWrite() returned TRUE");
+
+        //  Close the FITS file, free memory
+        psFitsClose(fitsFileW);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmChipRead() tests
+    // Verify pmChipRead() with NULL pmChip param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(!pmChipRead(NULL, fitsFileR, NULL), "pmChipRead() returned NULL with NULL pmChip param");
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipRead() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        ok(!pmChipRead(chip, NULL, NULL), "pmChipRead() returned NULL with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipRead() with acceptable input data
+    {
+        psMemId id = psMemGetId();
+        bool rc;
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        // Free the cells for chip 0 so we can verify that pmChipRead() actually reads the data from file
+        for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+            pmCell *cell = (pmCell *) chip->cells->data[chipID];
+            myFreeCell(cell);
+            cell = NULL;
+	}
+
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        rc = pmChipRead(chip, fitsFileR, NULL);
+        ok(rc, "pmChipRead() returned TRUE");
+        bool errorFlag = false;
+        // XXX: chipID should be cellID
+        for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+            if (VERBOSE) diag("Reading cell %d\n", chipID);
+            pmCell *cell = (pmCell *) chip->cells->data[chipID];
+            for (int k = 0 ; k < cell->hdu->images->n ; k++) {
+                if (VERBOSE) diag("Cool: image %d\n", k);
+                psImage *img = cell->hdu->images->data[k];
+                for (int i = 0 ; i < img->numRows ; i++) {
+                    for (int j = 0 ; j < img->numCols ; j++) {
+                        if (((float) (BASE_IMAGE+k)) != img->data.F32[i][j]) {
+                            diag("TEST ERROR: img[%d][%d] is %.2f, should be %.2f\n", i, j,
+                                  img->data.F32[i][j], ((float) (BASE_IMAGE+k)));
+                            errorFlag = true;
+			}
+		    }
+		}
+	    }
+            ok(!errorFlag, "pmChipWrite()/pmChipRead() properly set the image data (cell %d)", chipID);
+	}
+
+        psFitsClose(fitsFileR);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmChipWriteWeight() tests
+    // Verify pmChipWriteWeight() with NULL pmChip param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        ok(!pmChipWriteWeight(NULL, fitsFileW, NULL, false, true), "pmChipWriteWeight() returned NULL with NULL pmChip param");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipWriteWeight() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(!pmChipWriteWeight(chip, NULL, NULL, false, true), "pmChipWriteWeight() returned NULL with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipWriteWeight() with acceptable data
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        ok(chip != NULL, "Allocated a pmChip successfully");
+
+        //  Use pmChipWriteWeight() to write image data to the FITS file
+        bool rc = pmChipWriteWeight(chip, fitsFileW, NULL, false, true);
+        ok(rc, "pmChipWriteWeight() returned TRUE");
+
+        //  Close the FITS file, free memory
+        psFitsClose(fitsFileW);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmChipReadMask() tests
+    // Verify pmChipReadMask() with NULL pmChip param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(!pmChipReadMask(NULL, fitsFileR, NULL), "pmChipReadMask() returned NULL with NULL pmChip param");
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipReadMask() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        ok(!pmChipReadMask(chip, NULL, NULL), "pmChipReadMask() returned NULL with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipReadMask() with acceptable input data
+    {
+        psMemId id = psMemGetId();
+        bool rc;
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        // Free the cells for chip 0 so we can verify that pmChipReadMask() actually reads the data from file
+        for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+            pmCell *cell = (pmCell *) chip->cells->data[chipID];
+            myFreeCell(cell);
+            cell = NULL;
+	}
+
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        rc = pmChipReadMask(chip, fitsFileR, NULL);
+        ok(rc, "pmChipReadMask() returned TRUE");
+        bool errorFlag = false;
+        // XXX: chipID should be cellID
+        for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+            if (VERBOSE) diag("Reading cell %d\n", chipID);
+            pmCell *cell = (pmCell *) chip->cells->data[chipID];
+            for (int k = 0 ; k < cell->hdu->weights->n ; k++) {
+                if (VERBOSE) diag("Cool: image %d\n", k);
+                psImage *wgt = cell->hdu->weights->data[k];
+                for (int i = 0 ; i < wgt->numRows ; i++) {
+                    for (int j = 0 ; j < wgt->numCols ; j++) {
+                        if (((float) (BASE_WEIGHT+k)) != wgt->data.F32[i][j]) {
+                            diag("TEST ERROR: wgt[%d][%d] is %.2f, should be %.2f\n", i, j,
+                                  wgt->data.F32[i][j], ((float) (BASE_WEIGHT+k)));
+                            errorFlag = true;
+			}
+		    }
+		}
+	    }
+            ok(!errorFlag, "pmChipWriteWeight()/pmChipReadWeight() properly set the weight data (cell %d)", chipID);
+	}
+
+        psFitsClose(fitsFileR);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmChipReadMask() tests
+    // Verify pmChipReadMask() with NULL pmChip param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(!pmChipReadMask(NULL, fitsFileR, NULL), "pmChipReadMask() returned NULL with NULL pmChip param");
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipReadMask() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        ok(!pmChipReadMask(chip, NULL, NULL), "pmChipReadMask() returned NULL with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmChipReadMask() with acceptable input data
+    {
+        psMemId id = psMemGetId();
+        bool rc;
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        // Free the cells for chip 0 so we can verify that pmChipReadMask() actually reads the data from file
+        for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+            pmCell *cell = (pmCell *) chip->cells->data[chipID];
+            myFreeCell(cell);
+            cell = NULL;
+	}
+
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        rc = pmChipReadMask(chip, fitsFileR, NULL);
+        ok(rc, "pmChipReadMask() returned TRUE");
+        bool errorFlag = false;
+        // XXX: chipID should be cellID
+        for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+            if (VERBOSE) diag("Reading cell %d\n", chipID);
+            pmCell *cell = (pmCell *) chip->cells->data[chipID];
+            for (int k = 0 ; k < cell->hdu->masks->n ; k++) {
+                if (VERBOSE) diag("Cool: image %d\n", k);
+                psImage *msk = cell->hdu->masks->data[k];
+                for (int i = 0 ; i < msk->numRows ; i++) {
+                    for (int j = 0 ; j < msk->numCols ; j++) {
+                        if (((BASE_MASK+k)) != msk->data.U8[i][j]) {
+                            diag("TEST ERROR: msk[%d][%d] is %.2f, should be %.2f\n", i, j,
+                                  msk->data.F32[i][j], ((float) (BASE_MASK+k)));
+                            errorFlag = true;
+			}
+		    }
+		}
+	    }
+            ok(!errorFlag, "pmChipWriteMask()/pmChipReadMask() properly set the mask data (cell %d)", chipID);
+	}
+
+        psFitsClose(fitsFileR);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmFPAWrite() tests
+    // pmFPAWrite(pmFPA *fpa, psFits *fits, psDB *db, bool blank, bool recurse)
+    // Verify pmFPAWrite() with NULL pmFPA param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        ok(!pmFPAWrite(NULL, fitsFileW, NULL, false, true), "pmFPAWrite() returned NULL with NULL pmFPA param");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAWrite() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(!pmFPAWrite(fpa, NULL, NULL, false, true), "pmFPAWrite() returned NULL with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAWrite() with acceptable data
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        bool rc = pmFPAWrite(fpa, fitsFileW, NULL, false, true);
+        ok(rc, "pmFPAWrite() returned TRUE");
+        //  Close the FITS file, free memory
+        psFitsClose(fitsFileW);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPARead() tests
+    // Verify pmFPARead() with NULL pmFPA param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(!pmFPARead(NULL, fitsFileR, NULL), "pmFPARead() returned NULL with NULL pmFPA param");
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPARead() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(!pmFPARead(fpa, NULL, NULL), "pmFPARead() returned NULL with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPARead() with acceptable input data
+    {
+        psMemId id = psMemGetId();
+        bool rc;
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        // Free the cells for chip 0 so we can verify that pmFPARead() actually reads the data from file
+        if (0) {
+            for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+                pmCell *cell = (pmCell *) chip->cells->data[chipID];
+                myFreeCell(cell);
+                cell = NULL;
+            }
+	}
+
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        rc = pmFPARead(fpa, fitsFileR, NULL);
+        ok(rc, "pmFPARead() returned TRUE");
+        bool errorFlag = false;
+        // XXX: fpaID should be chipID
+        // XXX: chipID should be cellID
+        for (int fpaID = 0 ; fpaID < fpa->chips->n ; fpaID++) {
+            pmChip *chip = fpa->chips->data[fpaID];
+            for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+                if (VERBOSE) diag("Reading cell %d\n", chipID);
+                pmCell *cell = (pmCell *) chip->cells->data[chipID];
+                for (int k = 0 ; k < cell->hdu->images->n ; k++) {
+                    if (VERBOSE) diag("Cool: image %d\n", k);
+                    psImage *img = cell->hdu->images->data[k];
+                    for (int i = 0 ; i < img->numRows ; i++) {
+                        for (int j = 0 ; j < img->numCols ; j++) {
+                            if (((float) (BASE_IMAGE+k)) != img->data.F32[i][j]) {
+                                diag("TEST ERROR: img[%d][%d] is %.2f, should be %.2f\n", i, j,
+                                      img->data.F32[i][j], ((float) (BASE_IMAGE+k)));
+                                errorFlag = true;
+        			}
+        		    }
+        		}
+        	    }
+                ok(!errorFlag, "pmFPAWrite()/pmFPARead() properly set the image data (chip %d, cell %d)", fpaID, chipID);
+    	    }
+	}
+
+        psFitsClose(fitsFileR);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmFPAWriteWeight() tests
+    // pmFPAWriteWeight(pmFPA *fpa, psFits *fits, psDB *db, bool blank, bool recurse)
+    // Verify pmFPAWriteWeight() with NULL pmFPA param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        ok(!pmFPAWriteWeight(NULL, fitsFileW, NULL, false, true), "pmFPAWriteWeight() returned NULL with NULL pmFPA param");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAWriteWeight() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(!pmFPAWriteWeight(fpa, NULL, NULL, false, true), "pmFPAWriteWeight() returned NULL with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAWriteWeight() with acceptable data
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        bool rc = pmFPAWriteWeight(fpa, fitsFileW, NULL, false, true);
+        ok(rc, "pmFPAWriteWeight() returned TRUE");
+        //  Close the FITS file, free memory
+        psFitsClose(fitsFileW);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAReadWeight() tests
+    // Verify pmFPAReadWeight() with NULL pmFPA param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(!pmFPARead(NULL, fitsFileR, NULL), "pmFPAReadWeight() returned NULL with NULL pmFPA param");
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAReadWeight() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(!pmFPARead(fpa, NULL, NULL), "pmFPAReadWeight() returned NULL with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAReadWeight() with acceptable input data
+    {
+        psMemId id = psMemGetId();
+        bool rc;
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        // Free the cells for chip 0 so we can verify that pmFPAReadWeight() actually reads the data from file
+        if (0) {
+            for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+                pmCell *cell = (pmCell *) chip->cells->data[chipID];
+                myFreeCell(cell);
+                cell = NULL;
+            }
+	}
+
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        rc = pmFPARead(fpa, fitsFileR, NULL);
+        ok(rc, "pmFPAReadWeight() returned TRUE");
+        bool errorFlag = false;
+        // XXX: fpaID should be chipID
+        // XXX: chipID should be cellID
+        for (int fpaID = 0 ; fpaID < fpa->chips->n ; fpaID++) {
+            pmChip *chip = fpa->chips->data[fpaID];
+            for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+                if (VERBOSE) diag("Reading cell %d\n", chipID);
+                pmCell *cell = (pmCell *) chip->cells->data[chipID];
+                for (int k = 0 ; k < cell->hdu->weights->n ; k++) {
+                    if (VERBOSE) diag("Cool: image %d\n", k);
+                    psImage *wgt = cell->hdu->weights->data[k];
+                    for (int i = 0 ; i < wgt->numRows ; i++) {
+                        for (int j = 0 ; j < wgt->numCols ; j++) {
+                            if (((float) (BASE_WEIGHT+k)) != wgt->data.F32[i][j]) {
+                                diag("TEST ERROR: wgt[%d][%d] is %.2f, should be %.2f\n", i, j,
+                                      wgt->data.F32[i][j], ((float) (BASE_WEIGHT+k)));
+                                errorFlag = true;
+        			}
+        		    }
+        		}
+        	    }
+                ok(!errorFlag, "pmFPAWriteWeight()/pmFPAReadWeight() properly set the image data (chip %d, cell %d)", fpaID, chipID);
+    	    }
+	}
+
+        psFitsClose(fitsFileR);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // ----------------------------------------------------------------------
+    // pmFPAWriteMask() tests
+    // pmFPAWriteMask(pmFPA *fpa, psFits *fits, psDB *db, bool blank, bool recurse)
+    // Verify pmFPAWriteMask() with NULL pmFPA param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        ok(!pmFPAWriteMask(NULL, fitsFileW, NULL, false, true), "pmFPAWriteMask() returned NULL with NULL pmFPA param");
+        psFitsClose(fitsFileW);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAWriteMask() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(!pmFPAWriteMask(fpa, NULL, NULL, false, true), "pmFPAWriteMask() returned NULL with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAWriteMask() with acceptable data
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(".tmp01", "w");
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        bool rc = pmFPAWriteMask(fpa, fitsFileW, NULL, false, true);
+        ok(rc, "pmFPAWriteMask() returned TRUE");
+        //  Close the FITS file, free memory
+        psFitsClose(fitsFileW);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAReadMask() tests
+    // Verify pmFPAReadMask() with NULL pmFPA param
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(!pmFPARead(NULL, fitsFileR, NULL), "pmFPAReadMask() returned NULL with NULL pmFPA param");
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAReadMask() with NULL pmFits param
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        ok(!pmFPARead(fpa, NULL, NULL), "pmFPAReadMask() returned NULL with NULL pmFits param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Verify pmFPAReadMask() with acceptable input data
+    {
+        psMemId id = psMemGetId();
+        bool rc;
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        // Free the cells for chip 0 so we can verify that pmFPAReadMask() actually reads the data from file
+        if (0) {
+            for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+                pmCell *cell = (pmCell *) chip->cells->data[chipID];
+                myFreeCell(cell);
+                cell = NULL;
+            }
+	}
+
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        rc = pmFPARead(fpa, fitsFileR, NULL);
+        ok(rc, "pmFPAReadMask() returned TRUE");
+        bool errorFlag = false;
+        // XXX: fpaID should be chipID
+        // XXX: chipID should be cellID
+        for (int fpaID = 0 ; fpaID < fpa->chips->n ; fpaID++) {
+            pmChip *chip = fpa->chips->data[fpaID];
+            for (int chipID = 0 ; chipID < chip->cells->n ; chipID++) {
+                if (VERBOSE) diag("Reading cell %d\n", chipID);
+                pmCell *cell = (pmCell *) chip->cells->data[chipID];
+                for (int k = 0 ; k < cell->hdu->masks->n ; k++) {
+                    if (VERBOSE) diag("Cool: image %d\n", k);
+                    psImage *msk = cell->hdu->masks->data[k];
+                    for (int i = 0 ; i < msk->numRows ; i++) {
+                        for (int j = 0 ; j < msk->numCols ; j++) {
+                            if (((BASE_MASK+k)) != msk->data.U8[i][j]) {
+                                diag("TEST ERROR: msk[%d][%d] is %.2f, should be %.2f\n", i, j,
+                                      msk->data.F32[i][j], ((float) (BASE_MASK+k)));
+                                errorFlag = true;
+        			}
+        		    }
+        		}
+        	    }
+                ok(!errorFlag, "pmFPAWriteMask()/pmFPAReadMask() properly set the image data (chip %d, cell %d)", fpaID, chipID);
+    	    }
+	}
+
+        psFitsClose(fitsFileR);
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAUtils.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAUtils.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAUtils.c	(revision 15358)
@@ -0,0 +1,339 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+// XXX: Use better name for the temporary FITS file
+// XXX: The code to generate and free the FPA hierarchy was copied from
+// tap-pmFPA.c.  EIther include it directly, or library, or something.
+// Also, get rid of the manual free functions and use psFree() once
+// it correctly frees child members
+// XXX: For the genSimpleFPA() code, add IDs to each function so that
+// the values set in each chip-?cell-?hdu-?image are unique
+// XXX: For the genSimpleFPA() code, write masks and weights as well
+
+#define CHIP_ALLOC_NAME        "ChipName"
+#define CELL_ALLOC_NAME        "CellName"
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define MISC_NAME2             "META01"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           4
+#define TEST_NUM_COLS           4
+#define NUM_READOUTS            3
+#define NUM_CELLS               10
+#define NUM_CHIPS               8
+#define NUM_HDUS                5
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    chip->hdu = pmHDUAlloc("chipExtName");
+//
+//    bool rc = pmConfigFileRead(&chip->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleChip())");
+//    }
+//
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip);
+    }
+
+    // XXX: Add code to initialize chip pmConcepts
+
+
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    fpa->hdu = pmHDUAlloc("fpaExtName");
+//
+//    bool rc = pmConfigFileRead(&fpa->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+//    }
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa);
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+//    bool mdok;
+//    psMetadata *fileData = psMetadataLookupMetadata(&mdok, fpa->hdu->format, "FILE");
+//    char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+//    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, NULL, fpaNameHdr);
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(72);
+
+    // ----------------------------------------------------------------------
+    // int pmFPAFindChip(const pmFPA *fpa, const char *name)
+    {
+        psMemId id = psMemGetId();
+        int rc = pmFPAFindChip(NULL, "bogus");
+        ok(-1 == rc, "pmFPAFindChip() returns -1 with NULL pmFPA input param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // int pmFPAFindChip(const pmFPA *fpa, const char *name)
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        int rc = pmFPAFindChip(fpa, NULL);
+        ok(-1 == rc, "pmFPAFindChip() returns -1 with NULL name input param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // int pmFPAFindChip(const pmFPA *fpa, const char *name)
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        // Set unique chip names
+        for (int chipID = 0 ; chipID < fpa->chips->n ; chipID++) {
+            pmChip *chip = fpa->chips->data[chipID];
+            char tmpName[100];
+            sprintf(tmpName, "chip-%d", chipID);
+            psMetadataAddStr(chip->concepts, PS_LIST_TAIL, "CHIP.NAME", PS_META_REPLACE, NULL, tmpName);
+	}
+
+        // Verify that pmFPAFindChip() finds those chips correctly
+        for (int chipID = 0 ; chipID < fpa->chips->n ; chipID++) {
+            char tmpName[100];
+            sprintf(tmpName, "chip-%d", chipID);
+            int rc = pmFPAFindChip(fpa, tmpName);
+            ok(rc == chipID, "pmFPAFindChip() correctly found chip %s (%d)", tmpName, rc);
+	}
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // int pmChipFindCell(const pmChip *chip, const char *name)
+    {
+        psMemId id = psMemGetId();
+        int rc = pmChipFindCell(NULL, "bogus");
+        ok(-1 == rc, "pmChipFindCell() returns -1 with NULL pmChip input param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // int pmChipFindCell(const pmChip *chip, const char *name)
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmChip heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        int rc = pmChipFindCell(chip, NULL);
+        ok(-1 == rc, "pmChipFindCell() returns -1 with NULL name input param");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // int pmChipFindCell(const pmChip *chip, const char *name)
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmChip heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        // Set unique cell names
+        for (int cellID = 0 ; cellID < chip->cells->n ; cellID++) {
+            pmCell *cell = chip->cells->data[cellID];
+            char tmpName[100];
+            sprintf(tmpName, "cell-%d", cellID);
+            psMetadataAddStr(cell->concepts, PS_LIST_TAIL, "CELL.NAME", PS_META_REPLACE, NULL, tmpName);
+	}
+
+        // Verify that pmChipFindCell() finds those cells correctly
+        for (int cellID = 0 ; cellID < chip->cells->n ; cellID++) {
+            char tmpName[100];
+            sprintf(tmpName, "cell-%d", cellID);
+            int rc = pmChipFindCell(chip, tmpName);
+            ok(rc == cellID, "pmChipFindCell() correctly found cell %s (%d)", tmpName, rc);
+	}
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+}
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAView.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAView.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmFPAView.c	(revision 15358)
@@ -0,0 +1,989 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+// XXX: pmFPAviewThisHDU(): some problems with NULL pmHDUs
+// XXX: must test: pmFPAviewThisHDU(), pmFPAviewThisPHU()
+// XXX: must test: pmFPAviewThisPHU()
+
+#define	ERR_TRACE_LEVEL		0
+#define CHIP_ALLOC_NAME        "ChipName"
+#define CELL_ALLOC_NAME        "CellName"
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define MISC_NAME2             "META01"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           4
+#define TEST_NUM_COLS           4
+#define NUM_READOUTS            3
+#define NUM_CELLS               10
+#define NUM_CHIPS               8
+#define NUM_HDUS                5
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+
+    if (1) {
+        chip->hdu = pmHDUAlloc("chipExtName");
+//        bool rc = pmConfigFileRead(&chip->hdu->format, "data/camera0/format0.config", "Camera format 0");
+//        if (!rc) {
+//            diag("pmConfigFileRead() was unsuccessful (from generateSimpleChip())");
+//        }
+    }
+
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip);
+    }
+
+    // XXX: Add code to initialize chip pmConcepts
+
+
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+
+    if (1) {
+        fpa->hdu = pmHDUAlloc("fpaExtName");
+        bool rc = pmConfigFileRead(&fpa->hdu->format, "data/camera0/format0.config", "Camera format 0");
+        if (!rc) {
+            diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+        }
+    }
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa);
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+//    bool mdok;
+//    psMetadata *fileData = psMetadataLookupMetadata(&mdok, fpa->hdu->format, "FILE");
+//    char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+//    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, NULL, fpaNameHdr);
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(137);
+
+    // ----------------------------------------------------------------------
+    // pmFPAViewAlloc() tests
+    // XXX: Add memCheckPtr() tests
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(tmpView != NULL, "pmFPAviewAlloc() returned non-NULL");
+        ok(tmpView->nRows == 32, "pmFPAviewAlloc() set ->nRows properly");
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAViewReset() tests: NULL input
+    {
+        psMemId id = psMemGetId();
+        ok(!pmFPAviewReset(NULL), "pmFPAviewReset() returned FALSE with NULL input");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // pmFPAViewReset() tests: acceptable input
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(tmpView != NULL, "pmFPAviewAlloc() returned non-NULL");
+        tmpView->chip = 16;
+        tmpView->cell = 16;
+        tmpView->readout = 16;
+        tmpView->iRows = 16;
+        ok(pmFPAviewReset(tmpView), "pmFPAviewReset() returned TRUE");
+        ok(tmpView->chip == -1, "pmFPAviewReset() set tmpView->chip to -1");
+        ok(tmpView->cell == -1, "pmFPAviewReset() set tmpView->cell to -1");
+        ok(tmpView->readout == -1, "pmFPAviewReset() set tmpView->readout to 0");
+        ok(tmpView->iRows == 0, "pmFPAviewReset() set tmpView->iRows to 0");
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAViewForLevel() tests: NULL input
+    // pmFPAview *pmFPAviewForLevel(pmFPALevel level, const pmFPAview *input)
+
+    {
+        psMemId id = psMemGetId();
+        ok(!pmFPAviewForLevel(PM_FPA_LEVEL_FPA, NULL), "pmFPAviewForLevel() returned FALSE with NULL input");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAViewForLevel() tests: acceptable input
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(tmpView != NULL, "pmFPAviewAlloc() returned non-NULL");
+        tmpView->chip = 16;
+        tmpView->cell = 16;
+        tmpView->readout = 16;
+        tmpView->iRows = 16;
+
+        pmFPAview *newView = pmFPAviewForLevel(PM_FPA_LEVEL_FPA, tmpView);
+        ok(newView, "pmFPAviewReset(PM_FPA_LEVEL_FPA) returned non-NULL");
+        ok(newView->chip == -1, "pmFPAviewReset() set tmpView->chip to -1");
+        psFree(newView);
+
+        newView = pmFPAviewForLevel(PM_FPA_LEVEL_CHIP, tmpView);
+        ok(newView, "pmFPAviewReset(PM_FPA_LEVEL_CHIP) returned non-NULL");
+        ok(newView->cell == -1, "pmFPAviewReset() set tmpView->cell to -1");
+        psFree(newView);
+
+        newView = pmFPAviewForLevel(PM_FPA_LEVEL_CELL, tmpView);
+        ok(newView, "pmFPAviewReset(PM_FPA_LEVEL_CELL) returned non-NULL");
+        ok(newView->readout == -1, "pmFPAviewReset() set tmpView->readout to 0");
+        psFree(newView);
+
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAViewLevel() tests: NULL input
+    // pmFPALevel pmFPAviewLevel(const pmFPAview *view)
+    {
+        psMemId id = psMemGetId();
+        ok(!pmFPAviewLevel(NULL), "pmFPAviewLevel() returned NULL with NULL input");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAViewLevel() tests: acceptable input
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(tmpView != NULL, "pmFPAviewAlloc() returned non-NULL");
+        tmpView->chip = 16;
+        tmpView->cell = 16;
+        tmpView->readout = 16;
+        tmpView->iRows = 16;
+
+        tmpView->chip = -1;
+        ok(PM_FPA_LEVEL_FPA == pmFPAviewLevel(tmpView), "pmFPAviewReset() returned PM_FPA_LEVEL_FPA");
+        tmpView->chip = 16;
+
+        tmpView->cell = -1;
+        ok(PM_FPA_LEVEL_CHIP == pmFPAviewLevel(tmpView), "pmFPAviewReset() returned PM_FPA_LEVEL_CHIP");
+        tmpView->cell = 16;
+
+        tmpView->readout = -1;
+        ok(PM_FPA_LEVEL_CELL == pmFPAviewLevel(tmpView), "pmFPAviewReset() returned PM_FPA_LEVEL_CELL");
+        tmpView->readout = 16;
+
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAviewThisChip() tests: NULL view input
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(NULL == pmFPAviewThisChip(NULL, fpa), "pmFPAviewThisChip(NULL, fpa) returned NULL");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewThisChip() tests: NULL pmFPA input
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(NULL == pmFPAviewThisChip(tmpView, NULL), "pmFPAviewThisChip(pmFPAView, NULL) returned NULL");
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewThisChip() tests: NULL fpa->chips input
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        psArray *chips = fpa->chips;
+        fpa->chips = NULL;
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(NULL == pmFPAviewThisChip(tmpView, fpa), "pmFPAviewThisChip(pmFPAView, fpa) returned NULL view input");
+        fpa->chips = chips;
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // pmFPAviewThisChip() tests: acceptable input
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+
+        // Test with tmpView->chip too low
+        tmpView->chip = -1;
+        ok(NULL == pmFPAviewThisChip(tmpView, fpa), "pmFPAviewThisChip(pmFPAView, fpa) returned NULL with pmFPAView->chip == -1");
+
+        // Test with tmpView->chip too high
+        tmpView->chip = fpa->chips->n;
+        ok(NULL == pmFPAviewThisChip(tmpView, fpa), "pmFPAviewThisChip(pmFPAView, fpa) returned NULL with pmFPAView->chip larger than the number of chips");
+    
+        // Test with various tmpView->chip 
+        bool errorFlag = false;
+        for (int i = 0 ; i < NUM_CHIPS ; i++) {
+            tmpView->chip = i;
+            pmChip *tmpChip = pmFPAviewThisChip(tmpView, fpa);
+            if (!tmpChip || tmpChip != fpa->chips->data[i]) {
+                diag("ERROR: pmFPAviewThisChip() returned the incorrect chip (%d)", i);
+                errorFlag = true;
+	    }
+	}
+        ok(!errorFlag, "pmFPAviewThisChip() returned the correct pmChip for all tests");
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAviewNextChip() tests: NULL view input
+    // pmChip *pmFPAviewNextChip(pmFPAview *view, const pmFPA *fpa, int nStep)
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(NULL == pmFPAviewNextChip(NULL, fpa, 0), "pmFPAviewNextChip(NULL, fpa, 0) returned NULL");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewNextChip() tests: NULL pmFPA input
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(NULL == pmFPAviewNextChip(tmpView, NULL, 0), "pmFPAviewNextChip(pmFPAView, NULL, 0) returned NULL");
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewNextChip() tests: acceptable input
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+
+        // Test with tmpView->chip too low
+        // XXX: Source code is wrong: must guard against input tmpView->chip = -1
+        tmpView->chip = -1;
+        ok(NULL == pmFPAviewNextChip(tmpView, fpa, 0), "pmFPAviewNextChip(pmFPAView, fpa, 0) returned NULL with pmFPAView->chip == -1");
+
+        // Test with tmpView->chip too high
+        tmpView->chip = fpa->chips->n;
+        ok(NULL == pmFPAviewNextChip(tmpView, fpa, 0), "pmFPAviewNextChip(pmFPAView, fpa, 0) returned NULL with pmFPAView->chip larger than the number of chips");
+    
+        // Test with various tmpView->chip, step = 0
+        bool errorFlag = false;
+        for (int i = 0 ; i < NUM_CHIPS ; i++) {
+            tmpView->chip = i;
+            pmChip *tmpChip = pmFPAviewNextChip(tmpView, fpa, 0);
+            if (!tmpChip || tmpChip != fpa->chips->data[i]) {
+                diag("ERROR: pmFPAviewNextChip() returned the incorrect chip (%d)", i);
+                errorFlag = true;
+	    }
+	}
+        ok(!errorFlag, "pmFPAviewNextChip() returned the correct pmChip for all tests (step = 0)");
+
+        // Test with various tmpView->chip, step = 1
+        errorFlag = false;
+        for (int i = 0 ; i < NUM_CHIPS-1 ; i++) {
+            tmpView->chip = i;
+            pmChip *tmpChip = pmFPAviewNextChip(tmpView, fpa, 1);
+            if (!tmpChip || tmpChip != fpa->chips->data[i+1]) {
+                diag("ERROR: pmFPAviewNextChip() returned the incorrect chip (%d)", i);
+                errorFlag = true;
+	    }
+	}
+        ok(!errorFlag, "pmFPAviewNextChip() returned the correct pmChip for all tests (step = 1)");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAviewThisCell() tests: NULL view input
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(NULL == pmFPAviewThisCell(NULL, fpa), "pmFPAviewThisCell(NULL, fpa) returned NULL");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewThisCell() tests: NULL pmFPA input
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(NULL == pmFPAviewThisCell(tmpView, NULL), "pmFPAviewThisCell(pmFPAView, NULL) returned NULL");
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewThisCell() tests: NULL fpa->chips input
+    // XXX: Skipping because the source code does not guard against this
+    if (0) {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        psArray *chips = fpa->chips;
+        fpa->chips = NULL;
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(NULL == pmFPAviewThisCell(tmpView, fpa), "pmFPAviewThisCell(pmFPAView, fpa) returned NULL view input");
+        fpa->chips = chips;
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // pmFPAviewThisCell() tests: acceptable input
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+
+        // Test with tmpView->chip too low
+        tmpView->chip = -1;
+        ok(NULL == pmFPAviewThisCell(tmpView, fpa), "pmFPAviewThisCell(pmFPAView, fpa) returned NULL with pmFPAView->chip == -1");
+
+        // Test with tmpView->chip too high
+        tmpView->chip = fpa->chips->n;
+        ok(NULL == pmFPAviewThisCell(tmpView, fpa), "pmFPAviewThisCell(pmFPAView, fpa) returned NULL with pmFPAView->chip larger than the number of chips");
+    
+        // Test with various tmpView->chip 
+        bool errorFlag = false;
+        for (int i = 0 ; i < NUM_CHIPS ; i++) {
+            pmChip *chip = fpa->chips->data[i];
+            for (int j = 0 ; j < NUM_CELLS ; j++) {
+                tmpView->chip = i;
+                tmpView->cell = j;
+                pmCell *tmpCell = pmFPAviewThisCell(tmpView, fpa);
+                if (!tmpCell || tmpCell != chip->cells->data[j]) {
+                    diag("ERROR: pmFPAviewThisCell() returned the incorrect chip/cell (%d, %d)", i, j);
+                    errorFlag = true;
+		}
+
+    	    }
+	}
+        ok(!errorFlag, "pmFPAviewThisCell() returned the correct pmCell for all tests");
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAviewNextCell() tests: NULL view input
+    // pmChip *pmFPAviewNextCell(pmFPAview *view, const pmFPA *fpa, int nStep)
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(NULL == pmFPAviewNextCell(NULL, fpa, 0), "pmFPAviewNextCell(NULL, fpa, 0) returned NULL");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewNextCell() tests: NULL pmFPA input
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(NULL == pmFPAviewNextCell(tmpView, NULL, 0), "pmFPAviewNextCell(pmFPAView, NULL, 0) returned NULL");
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewNextCell() tests: acceptable input
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+
+        // Test with tmpView->chip too low
+        tmpView->chip = -1;
+        ok(NULL == pmFPAviewNextCell(tmpView, fpa, 0), "pmFPAviewNextCell(pmFPAView, fpa) returned NULL with pmFPAView->chip == -1");
+
+        // Test with tmpView->chip too high
+        tmpView->chip = fpa->chips->n;
+        ok(NULL == pmFPAviewNextCell(tmpView, fpa, 0), "pmFPAviewNextCell(pmFPAView, fpa) returned NULL with pmFPAView->chip larger than the number of chips");
+    
+        // Test with various tmpView->chip (step = 0)
+        bool errorFlag = false;
+        for (int i = 0 ; i < NUM_CHIPS ; i++) {
+            pmChip *chip = fpa->chips->data[i];
+            for (int j = 0 ; j < NUM_CELLS ; j++) {
+                tmpView->chip = i;
+                tmpView->cell = j;
+                pmCell *tmpCell = pmFPAviewNextCell(tmpView, fpa, 0);
+                if (!tmpCell || tmpCell != chip->cells->data[j]) {
+                    diag("ERROR: pmFPAviewNextCell() returned the incorrect chip/cell (%d, %d)", i, j);
+                    errorFlag = true;
+		}
+
+    	    }
+	}
+        ok(!errorFlag, "pmFPAviewNextCell() returned the correct pmCell for all tests (step = 0)");
+
+        // Test with various tmpView->chip (step = 1)
+        errorFlag = false;
+        for (int i = 0 ; i < NUM_CHIPS ; i++) {
+            pmChip *chip = fpa->chips->data[i];
+            for (int j = 0 ; j < NUM_CELLS-1 ; j++) {
+                tmpView->chip = i;
+                tmpView->cell = j;
+                pmCell *tmpCell = pmFPAviewNextCell(tmpView, fpa, 1);
+                if (!tmpCell || tmpCell != chip->cells->data[j+1]) {
+                    diag("ERROR: pmFPAviewNextCell() returned the incorrect chip/cell (%d, %d)", i, j);
+                    errorFlag = true;
+		}
+
+    	    }
+	}
+        ok(!errorFlag, "pmFPAviewNextCell() returned the correct pmCell for all tests (step = 1)");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAviewThisReadout() tests: NULL view input
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(NULL == pmFPAviewThisReadout(NULL, fpa), "pmFPAviewThisReadout(NULL, fpa) returned NULL");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewThisReadout() tests: NULL pmFPA input
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(NULL == pmFPAviewThisReadout(tmpView, NULL), "pmFPAviewThisReadout(pmFPAView, NULL) returned NULL");
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewThisReadout() tests: NULL fpa->chips input
+    // XXX: Skipping because the source code does not guard against this
+    if (0) {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        psArray *chips = fpa->chips;
+        fpa->chips = NULL;
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(NULL == pmFPAviewThisReadout(tmpView, fpa), "pmFPAviewThisReadout(pmFPAView, fpa) returned NULL view input");
+        fpa->chips = chips;
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // pmFPAviewThisReadout() tests: acceptable input
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+
+        // Test with tmpView->chip too low
+        tmpView->chip = -1;
+        ok(NULL == pmFPAviewThisReadout(tmpView, fpa), "pmFPAviewThisReadout(pmFPAView, fpa) returned NULL with pmFPAView->chip == -1");
+
+        // Test with tmpView->chip too high
+        tmpView->chip = fpa->chips->n;
+        ok(NULL == pmFPAviewThisReadout(tmpView, fpa), "pmFPAviewThisReadout(pmFPAView, fpa) returned NULL with pmFPAView->chip larger than the number of chips");
+    
+        // Test with various tmpView->chip 
+        bool errorFlag = false;
+        for (int i = 0 ; i < NUM_CHIPS ; i++) {
+            pmChip *chip = fpa->chips->data[i];
+            for (int j = 0 ; j < NUM_CELLS ; j++) {
+                pmCell *cell = chip->cells->data[j];
+                for (int k = 0 ; k < NUM_READOUTS ; k++) {
+                    tmpView->chip = i;
+                    tmpView->cell = j;
+                    tmpView->readout = k;
+                    pmReadout *tmpReadout = pmFPAviewThisReadout(tmpView, fpa);
+                    if (!tmpReadout || tmpReadout != cell->readouts->data[k]) {
+                        diag("ERROR: pmFPAviewThisReadout() returned the incorrect chip/cell/readout (%d, %d, %d)", i, j, k);
+                        errorFlag = true;
+		    }
+		}
+    	    }
+	}
+        ok(!errorFlag, "pmFPAviewThisReadout() returned the correct pmCell for all tests");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAviewNextReadout() tests: NULL view input
+    // pmChip *pmFPAviewNextReadout(pmFPAview *view, const pmFPA *fpa, int nStep)
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(NULL == pmFPAviewNextReadout(NULL, fpa, 0), "pmFPAviewNextReadout(NULL, fpa, 0) returned NULL");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmFPAviewNextReadout() tests: NULL pmFPA input
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(NULL == pmFPAviewNextReadout(tmpView, NULL, 0), "pmFPAviewNextReadout(pmFPAView, NULL, 0) returned NULL");
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // pmFPAviewNextReadout() tests: acceptable input
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+
+        // Test with tmpView->chip too low
+        tmpView->chip = -1;
+        ok(NULL == pmFPAviewNextReadout(tmpView, fpa, 0), "pmFPAviewNextReadout(pmFPAView, fpa) returned NULL with pmFPAView->chip == -1");
+
+        // Test with tmpView->chip too high
+        tmpView->chip = fpa->chips->n;
+        ok(NULL == pmFPAviewNextReadout(tmpView, fpa, 0), "pmFPAviewNextReadout(pmFPAView, fpa) returned NULL with pmFPAView->chip larger than the number of chips");
+    
+        // Test with various tmpView->chip (step = 0)
+        bool errorFlag = false;
+        for (int i = 0 ; i < NUM_CHIPS ; i++) {
+            pmChip *chip = fpa->chips->data[i];
+            for (int j = 0 ; j < NUM_CELLS ; j++) {
+                pmCell *cell = chip->cells->data[j];
+                for (int k = 0 ; k < NUM_READOUTS ; k++) {
+                    tmpView->chip = i;
+                    tmpView->cell = j;
+                    tmpView->readout = k;
+                    pmReadout *tmpReadout = pmFPAviewNextReadout(tmpView, fpa, 0);
+                    if (!tmpReadout || tmpReadout != cell->readouts->data[k]) {
+                        diag("ERROR: pmFPAviewNextReadout() returned the incorrect chip/cell/readout (%d, %d, %d)", i, j, k);
+                        errorFlag = true;
+		    }
+		}
+    	    }
+	}
+        ok(!errorFlag, "pmFPAviewNextReadout() returned the correct pmCell for all tests (step = 0)");
+
+        // Test with various tmpView->chip (step = 1)
+        errorFlag = false;
+        for (int i = 0 ; i < NUM_CHIPS ; i++) {
+            pmChip *chip = fpa->chips->data[i];
+            for (int j = 0 ; j < NUM_CELLS ; j++) {
+                pmCell *cell = chip->cells->data[j];
+                for (int k = 0 ; k < NUM_READOUTS-1 ; k++) {
+                    tmpView->chip = i;
+                    tmpView->cell = j;
+                    tmpView->readout = k;
+                    pmReadout *tmpReadout = pmFPAviewNextReadout(tmpView, fpa, 1);
+                    if (!tmpReadout || tmpReadout != cell->readouts->data[k+1]) {
+                        diag("ERROR: pmFPAviewNextReadout() returned the incorrect chip/cell/readout (%d, %d, %d)", i, j, k);
+                        errorFlag = true;
+		    }
+		}
+    	    }
+	}
+        ok(!errorFlag, "pmFPAviewNextReadout() returned the correct pmCell for all tests (step = 1)");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmFPAviewThisHDU() tests: NULL view input
+    // pmHDU *pmFPAviewThisHDU(const pmFPAview *view, const pmFPA *fpa)
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(NULL == pmFPAviewThisHDU(NULL, fpa), "pmFPAviewThisHDU(NULL, fpa) returned NULL");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // pmFPAviewThisHDU() tests: NULL pmFPA input
+    // pmHDU *pmFPAviewThisHDU(const pmFPAview *view, const pmFPA *fpa)
+    {
+        psMemId id = psMemGetId();
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+        ok(NULL == pmFPAviewThisHDU(tmpView, NULL), "pmFPAviewThisHDU(pmFPAView, NULL) returned NULL");
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+    // pmFPAviewThisHDU() tests: acceptable input
+    // pmHDU *pmFPAviewThisHDU(const pmFPAview *view, const pmFPA *fpa)
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmFPAview *tmpView = pmFPAviewAlloc(32);
+
+        tmpView->chip = -1;
+        pmHDU *hdu = pmFPAviewThisHDU(tmpView, fpa);
+        ok(hdu, "pmFPAviewThisHDU() returned non-NULL with tmpView->chip = -1");
+        ok(hdu == pmHDUFromFPA(fpa), "pmFPAviewThisHDU() returned the correct HDU");
+        tmpView->chip = 16;
+
+        // XXX: These tests fail because the pmHDU functions are returning NULL.  Why? 
+        tmpView->cell = -1;
+        hdu = pmFPAviewThisHDU(tmpView, fpa);
+        ok(hdu, "pmFPAviewThisHDU() returned non-NULL with tmpView->cell = -1");
+        ok(hdu == pmHDUFromChip(pmFPAviewThisChip(tmpView, fpa)), "pmFPAviewThisHDU() returned the correct HDU");
+        tmpView->cell = 16;
+
+        tmpView->readout = -1;
+        hdu = pmFPAviewThisHDU(tmpView, fpa);
+        ok(hdu, "pmFPAviewThisHDU() returned non-NULL with tmpView->readout = -1");
+        ok(hdu == pmHDUFromCell(pmFPAviewThisCell(tmpView, fpa)), "pmFPAviewThisHDU() returned the correct HDU");
+        tmpView->readout = 16;
+
+        hdu = pmFPAviewThisHDU(tmpView, fpa);
+        ok(hdu, "pmFPAviewThisHDU() returned non-NULL");
+        ok(hdu == pmHDUFromReadout(pmFPAviewThisReadout(tmpView, fpa)), "pmFPAviewThisHDU() returned the correct HDU");
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        psFree(tmpView);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmHDU.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmHDU.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmHDU.c	(revision 15358)
@@ -0,0 +1,544 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/*
+    The following psModule functions are tested:
+        pmHDUAlloc()
+        pmHDUReadHeader()
+        pmHDURead()
+        pmHDUReadMask()
+        pmHDUReadWeight()
+        pmHDUWrite()
+        pmHDUWriteMask()
+        pmHDUWriteWeight()
+*/
+
+#define NUM_HDUS 8
+char *fitsFilename = "tmp.fits";
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", 0);
+    plan_tests(159);
+
+    // ----------------------------------------------------------------------
+    // pmHDUAlloc() tests
+    // Test pmHDUAlloc() with a NULL extname
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUAlloc(NULL);
+        ok(hdu, "pmHDUAlloc(NULL) returned non-NULL");
+        skip_start(!hdu, 7, "Skipping tests because pmHDUAlloc(NULL) returned NULL");
+        ok(hdu->blankPHU == true, "pmHDUAlloc(NULL) set hdu->blankPHU correctly");
+        ok(hdu->extname == NULL, "pmHDUAlloc(NULL) set hdu->extname correctly");
+        ok(hdu->format == NULL, "pmHDUAlloc(NULL) set hdu->format correctly");
+        ok(hdu->header == NULL, "pmHDUAlloc(NULL) set hdu->header correctly");
+        ok(hdu->images == NULL, "pmHDUAlloc(NULL) set hdu->images correctly");
+        ok(hdu->weights == NULL, "pmHDUAlloc(NULL) set hdu->weights correctly");
+        ok(hdu->masks == NULL, "pmHDUAlloc(NULL) set hdu->masks correctly");
+        psFree(hdu);
+        skip_end();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDUAlloc() with a non-NULL extname
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUAlloc("ext-0");
+        ok(hdu, "pmHDUAlloc(extname) returned non-NULL");
+        skip_start(!hdu, 7, "Skipping tests because pmHDUAlloc(extname) returned NULL");
+        ok(hdu->blankPHU == false, "pmHDUAlloc(extname) set hdu->blankPHU correctly");
+        ok(0 == strcmp(hdu->extname, "ext-0"), "pmHDUAlloc(extname) set hdu->extname correctly");
+        ok(hdu->format == NULL, "pmHDUAlloc(extname) set hdu->format correctly");
+        ok(hdu->header == NULL, "pmHDUAlloc(extname) set hdu->header correctly");
+        ok(hdu->images == NULL, "pmHDUAlloc(extname) set hdu->images correctly");
+        ok(hdu->weights == NULL, "pmHDUAlloc(extname) set hdu->weights correctly");
+        ok(hdu->masks == NULL, "pmHDUAlloc(extname) set hdu->masks correctly");
+        psFree(hdu);
+        skip_end();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmHDUWrite(), pmHDURead tests
+    // Test pmHDUWrite() with NULL pmHDU input argument
+    {
+        psMemId id = psMemGetId();
+        // Create simple FITS file
+        psFits* fitsFile = psFitsOpen(".tmp00", "w");
+        ok(fitsFile != NULL, "psFitsOpen() was successful");
+        psMetadata* header = psMetadataAlloc();
+        psImage* image = psImageAlloc(16, 16, PS_TYPE_F32);
+        ok(psFitsWriteImage(fitsFile, header, image, 0, "extname"), "psFitsWriteImage() successful");
+        psFree(header);
+        psFree(image);
+        bool rc = pmHDUWrite(NULL, fitsFile);
+        ok(rc == false, "pmHDUWrite() returned FALSE with NULL psHDU as input");
+        psFitsClose(fitsFile);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDUWrite() with NULL psFits input argument
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUAlloc("extname");
+        hdu->header = psMetadataAlloc();
+        psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYINT", PS_DATA_S32, "psS32 Item", 0);
+        bool rc = pmConfigFileRead(&hdu->format, "data/camera0/format0.config", "Camera 0 Config Format");
+        ok(rc == true, "pmConfigFileRead() was successful");
+        rc = pmHDUWrite(hdu, NULL);
+        ok(rc == false, "pmHDUWrite() returned FALSE with NULL psFits file as input");
+        psFree(hdu);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDURead() with NULL pmHDU input argument
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen("data/sampleFitsFile.fits", "r");
+        ok(fitsFile != NULL, "psFitsOpen() was successful");
+        bool rc = pmHDURead(NULL, fitsFile);
+        ok(rc == false, "pmHDURead() returned FALSE with NULL psHDU as input");
+        psFitsClose(fitsFile);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDURead() with NULL pmHDU input argument
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUAlloc("extname");
+        hdu->header = psMetadataAlloc();
+        bool rc = pmHDURead(hdu, NULL);
+        ok(rc == false, "pmHDURead() returned FALSE with NULL psFits file as input");
+        psFree(hdu);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // 1. Create a simple FITS file
+    // 2. Create a simple pmHDU header
+    // 3. Use pmHDUWrite() to write that header to the FITS file
+    // 4. Close the FITS file
+    // 5. Read that fits file from disk
+    // 6. Create another pmHDU
+    // 7. Call pmHDURead()which will read the FITS data into hdu->images
+    // 8. Verify that hdu->images was set correctly.
+    // 
+    // XXX: Add code to delete .tmp00
+    // XXX: Should we try with multiple images in the psArray hdu->images?
+    #define BASE 20
+    {
+        psMemId id = psMemGetId();
+        // 1. Create a simple FITS file
+        psFits* fitsFileW = psFitsOpen(".tmp00", "w");
+        ok(fitsFileW != NULL, "psFitsOpen() was successful");
+        // 2. Create a simple pmHDU header
+        //    Allocate and format hdu->header
+        //    Set hdu->images
+        pmHDU *hdu = pmHDUAlloc("extname");
+        bool rc = pmConfigFileRead(&hdu->format, "data/camera0/format0.config", "Camera format 0");
+        ok(rc, "pmConfigFileRead() was successful");
+        hdu->images = psArrayAlloc(NUM_HDUS);
+        for (int k = 0 ; k < NUM_HDUS ; k++) {
+            hdu->images->data[k] = psImageAlloc(4, 4, PS_TYPE_F32);
+            psImageInit(hdu->images->data[k], (float) (BASE+k));
+	}
+        // 3. Use pmHDUWrite() to write that header to the FITS file
+        rc = pmHDUWrite(hdu, fitsFileW);
+        ok(rc == true, "pmHDUWrite() returned TRUE");
+        // 4. Close the FITS file, free memory
+        psFree(hdu);
+        psFitsClose(fitsFileW);
+
+        // Now, try to read that FITS file from disk.
+        // 5. Read that fits file from disk
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(fitsFileR != NULL, "psFitsOpen() was successful");
+        // 6. Create another pmHDU
+        hdu = pmHDUAlloc("extname");
+        hdu->images = NULL;
+        // 7. Call pmHDURead()which will read the FITS data into hdu->images
+        rc = pmHDURead(hdu, fitsFileR);
+        ok(rc == true, "pmHDURead() returned TRUE");
+        ok(hdu->images != NULL, "pmHDURead() created hdu->images");
+        // 8. Verify that hdu->images was set correctly.
+        bool errorFlag = false;
+        for (int k = 0 ; k < NUM_HDUS ; k++) {
+            psImage *img = (psImage *) hdu->images->data[k];
+            for (psS32 i = 0 ; i < img->numRows ; i++) {
+                for (psS32 j = 0 ; j < img->numCols ; j++) {
+                    if (((float) k+BASE) != img->data.F32[i][j]) {
+                        errorFlag = true;
+                        diag("TEST ERROR (image %d): img[%d][%d] is %f, should be %f\n",
+                              k, i, j, img->data.F32[i][j], ((float)k+BASE));
+        	    }
+            	}
+	    }
+	}
+        ok(!errorFlag, "pmHDURead() correctly returned the image data");
+        psFitsClose(fitsFileR);
+        psFree(hdu);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmHDUWriteWeight(), pmHDUReadWeight() tests
+    // Test pmHDUWriteWeight() with NULL pmHDU input argument
+    {
+        psMemId id = psMemGetId();
+        // Create simple FITS file
+        psFits* fitsFile = psFitsOpen(".tmp00", "w");
+        ok(fitsFile != NULL, "psFitsOpen() was successful");
+        psMetadata* header = psMetadataAlloc();
+        psImage* image = psImageAlloc(16, 16, PS_TYPE_F32);
+        ok(psFitsWriteImage(fitsFile, header, image, 0, "extname"), "psFitsWriteImage() successful");
+        psFree(header);
+        psFree(image);
+        bool rc = pmHDUWriteWeight(NULL, fitsFile);
+        ok(rc == false, "pmHDUWriteWeight() returned FALSE with NULL psHDU as input");
+        psFitsClose(fitsFile);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDUWriteWeight() with NULL psFits input argument
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUAlloc("extname");
+        hdu->header = psMetadataAlloc();
+        psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYINT", PS_DATA_S32, "psS32 Item", 0);
+        bool rc = pmConfigFileRead(&hdu->format, "data/camera0/format0.config", "Camera 0 Config Format");
+        ok(rc == true, "pmConfigFileRead() was successful");
+        rc = pmHDUWriteWeight(hdu, NULL);
+        ok(rc == false, "pmHDUWriteWeight    () returned FALSE with NULL psFits file as input");
+        psFree(hdu);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDUReadWeight() with NULL pmHDU input argument
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen("data/sampleFitsFile.fits", "r");
+        ok(fitsFile != NULL, "psFitsOpen() was successful");
+        bool rc = pmHDUReadWeight(NULL, fitsFile);
+        ok(rc == false, "pmHDUReadWeight() returned FALSE with NULL psHDU as input");
+        psFitsClose(fitsFile);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDUReadWeight() with NULL pmHDU input argument
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUAlloc("extname");
+        hdu->header = psMetadataAlloc();
+        bool rc = pmHDUReadWeight(hdu, NULL);
+        ok(rc == false, "pmHDUReadWeight    () returned FALSE with NULL psFits file as input");
+        psFree(hdu);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // 1. Create a simple FITS file
+    // 2. Create a simple pmHDU header
+    // 3. Use pmHDUWriteWeight() to write that header to the FITS file
+    // 4. Close the FITS file
+    // 5. Read that fits file from disk
+    // 6. Create another pmHDU
+    // 7. Call pmHDUReadWeight()which will read the FITS data into hdu->images
+    // 8. Verify that hdu->images was set correctly.
+    // 
+    // XXX: Add code to delete .tmp00
+    // XXX: Should we try with multiple images in the psArray hdu->images?
+    #define BASE 20
+    {
+        psMemId id = psMemGetId();
+        // 1. Create a simple FITS file
+        psFits* fitsFileW = psFitsOpen(".tmp00", "w");
+        ok(fitsFileW != NULL, "psFitsOpen() was successful");
+        // 2. Create a simple pmHDU header
+        //    Allocate and format hdu->header
+        //    Set hdu->images
+        pmHDU *hdu = pmHDUAlloc("extname");
+        bool rc = pmConfigFileRead(&hdu->format, "data/camera0/format0.config", "Camera format 0");
+        ok(rc, "pmConfigFileRead() was successful");
+        hdu->weights = psArrayAlloc(NUM_HDUS);
+        for (int k = 0 ; k < NUM_HDUS ; k++) {
+            hdu->weights->data[k] = psImageAlloc(4, 4, PS_TYPE_F32);
+            psImageInit(hdu->weights->data[k], (float) (BASE+k));
+	}
+        // 3. Use pmHDUWriteWeight() to write that header to the FITS file
+        rc = pmHDUWriteWeight(hdu, fitsFileW);
+        ok(rc == true, "pmHDUWriteWeight() returned TRUE");
+        // 4. Close the FITS file, free memory
+        psFree(hdu);
+        psFitsClose(fitsFileW);
+
+        // Now, try to read that FITS file from disk.
+        // 5. Read that fits file from disk
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(fitsFileR != NULL, "psFitsOpen() was successful");
+        // 6. Create another pmHDU
+        hdu = pmHDUAlloc("extname");
+        hdu->weights = NULL;
+        // 7. Call pmHDUReadWeight()which will read the FITS data into hdu->weights
+        rc = pmHDUReadWeight(hdu, fitsFileR);
+        ok(rc == true, "pmHDUReadWeight() returned TRUE");
+        ok(hdu->weights != NULL, "pmHDUReadWeight() created hdu->weights");
+        // 8. Verify that hdu->weights was set correctly.
+        bool errorFlag = false;
+        for (int k = 0 ; k < NUM_HDUS ; k++) {
+            psImage *img = (psImage *) hdu->weights->data[k];
+            for (psS32 i = 0 ; i < img->numRows ; i++) {
+                for (psS32 j = 0 ; j < img->numCols ; j++) {
+                    if (((float) k+BASE) != img->data.F32[i][j]) {
+                        errorFlag = true;
+                        diag("TEST ERROR (image %d): img[%d][%d] is %f, should be %f\n",
+                              k, i, j, img->data.F32[i][j], ((float)k+BASE));
+        	    }
+            	}
+	    }
+	}
+        ok(!errorFlag, "pmHDUReadWeight() correctly returned the image data");
+        psFitsClose(fitsFileR);
+        psFree(hdu);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmHDUWriteMask(), pmHDUReadMask() tests
+    // Test pmHDUWriteMask() with NULL pmHDU input argument
+    {
+        psMemId id = psMemGetId();
+        // Create simple FITS file
+        psFits* fitsFile = psFitsOpen(".tmp00", "w");
+        ok(fitsFile != NULL, "psFitsOpen() was successful");
+        psMetadata* header = psMetadataAlloc();
+        psImage* image = psImageAlloc(16, 16, PS_TYPE_F32);
+        ok(psFitsWriteImage(fitsFile, header, image, 0, "extname"), "psFitsWriteImage() successful");
+        psFree(header);
+        psFree(image);
+        bool rc = pmHDUWriteMask(NULL, fitsFile);
+        ok(rc == false, "pmHDUWriteMask() returned FALSE with NULL psHDU as input");
+        psFitsClose(fitsFile);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDUWriteMask() with NULL psFits input argument
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUAlloc("extname");
+        hdu->header = psMetadataAlloc();
+        psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYINT", PS_DATA_S32, "psS32 Item", 0);
+        bool rc = pmConfigFileRead(&hdu->format, "data/camera0/format0.config", "Camera 0 Config Format");
+        ok(rc == true, "pmConfigFileRead() was successful");
+        rc = pmHDUWriteMask(hdu, NULL);
+        ok(rc == false, "pmHDUWriteMask() returned FALSE with NULL psFits file as input");
+        psFree(hdu);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDUReadMask() with NULL pmHDU input argument
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen("data/sampleFitsFile.fits", "r");
+        ok(fitsFile != NULL, "psFitsOpen() was successful");
+        bool rc = pmHDUReadMask(NULL, fitsFile);
+        ok(rc == false, "pmHDUReadMask() returned FALSE with NULL psHDU as input");
+        psFitsClose(fitsFile);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDUReadMask() with NULL pmHDU input argument
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUAlloc("extname");
+        hdu->header = psMetadataAlloc();
+        bool rc = pmHDUReadMask(hdu, NULL);
+        ok(rc == false, "pmHDUReadMask() returned FALSE with NULL psFits file as input");
+        psFree(hdu);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // 1. Create a simple FITS file
+    // 2. Create a simple pmHDU header
+    // 3. Use pmHDUWriteMask() to write that header to the FITS file
+    // 4. Close the FITS file
+    // 5. Read that fits file from disk
+    // 6. Create another pmHDU
+    // 7. Call pmHDUReadMask()which will read the FITS data into hdu->images
+    // 8. Verify that hdu->images was set correctly.
+    // 
+    // XXX: Add code to delete .tmp00
+    // XXX: Should we try with multiple images in the psArray hdu->images?
+    #define BASE 20
+    {
+        psMemId id = psMemGetId();
+        // 1. Create a simple FITS file
+        psFits* fitsFileW = psFitsOpen(".tmp00", "w");
+        ok(fitsFileW != NULL, "psFitsOpen() was successful");
+        // 2. Create a simple pmHDU header
+        //    Allocate and format hdu->header
+        //    Set hdu->images
+        pmHDU *hdu = pmHDUAlloc("extname");
+        bool rc = pmConfigFileRead(&hdu->format, "data/camera0/format0.config", "Camera format 0");
+        ok(rc, "pmConfigFileRead() was successful");
+        hdu->masks = psArrayAlloc(NUM_HDUS);
+        for (int k = 0 ; k < NUM_HDUS ; k++) {
+            hdu->masks->data[k] = psImageAlloc(4, 4, PS_TYPE_F32);
+            psImageInit(hdu->masks->data[k], (float) (BASE+k));
+	}
+        // 3. Use pmHDUWriteMask() to write that header to the FITS file
+        rc = pmHDUWriteMask(hdu, fitsFileW);
+        ok(rc == true, "pmHDUWriteMask() returned TRUE");
+        // 4. Close the FITS file, free memory
+        psFree(hdu);
+        psFitsClose(fitsFileW);
+
+        // Now, try to read that FITS file from disk.
+        // 5. Read that fits file from disk
+        psFits* fitsFileR = psFitsOpen(".tmp00", "r");
+        ok(fitsFileR != NULL, "psFitsOpen() was successful");
+        // 6. Create another pmHDU
+        hdu = pmHDUAlloc("extname");
+        hdu->masks = NULL;
+        // 7. Call pmHDUReadMask()which will read the FITS data into hdu->masks
+        rc = pmHDUReadMask(hdu, fitsFileR);
+        ok(rc == true, "pmHDUReadMask() returned TRUE");
+        ok(hdu->masks != NULL, "pmHDUReadMask() created hdu->masks");
+        // 8. Verify that hdu->masks was set correctly.
+        bool errorFlag = false;
+        for (int k = 0 ; k < NUM_HDUS ; k++) {
+            psImage *img = (psImage *) hdu->masks->data[k];
+            for (psS32 i = 0 ; i < img->numRows ; i++) {
+                for (psS32 j = 0 ; j < img->numCols ; j++) {
+                    if (((float) k+BASE) != img->data.F32[i][j]) {
+                        errorFlag = true;
+                        diag("TEST ERROR (image %d): img[%d][%d] is %f, should be %f\n",
+                              k, i, j, img->data.F32[i][j], ((float)k+BASE));
+        	    }
+            	}
+	    }
+	}
+        ok(!errorFlag, "pmHDUReadMask() correctly returned the image data");
+        psFitsClose(fitsFileR);
+        psFree(hdu);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmHDUReadHeader(), pmHDUWriteHeader() tests
+    // Test pmHDUReadHeader() with NULL pmHDU input argument
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFile = psFitsOpen("data/sampleFitsFile.fits", "r");
+        ok(fitsFile != NULL, "psFitsOpen() was successful");
+        bool rc = pmHDUReadHeader(NULL, fitsFile);
+        ok(rc == false, "pmHDUReadHeader() returned FALSE with NULL psHDU as input");
+        psFitsClose(fitsFile);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDUReadHeader() with NULL psFits input argument
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUAlloc("extname");
+        hdu->header = psMetadataAlloc();
+        bool rc = pmHDUReadHeader(hdu, NULL);
+        ok(rc == false, "pmHDUReadHeader() returned FALSE with NULL psFits file as input");
+        psFree(hdu);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmHDUReadHeader() and pmHDUWriteHeader()
+    {
+        psMemId id = psMemGetId();
+        psFits* fitsFileW = psFitsOpen(fitsFilename, "w");
+        ok(fitsFileW != NULL, "psFitsOpen() opened the FITS file");
+        char extname[80];
+        for (int lcv = 0; lcv < NUM_HDUS; lcv++) {
+            snprintf(extname, 80, "ext-%d", lcv);
+            pmHDU *hdu = pmHDUAlloc(extname);
+            hdu->header = psMetadataAlloc();
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYINT", PS_DATA_S32,
+                         "psS32 Item", (psS32)lcv);
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYFLT", PS_DATA_F32,
+                         "psF32 Item", (float)(1.0f/(float)(1+lcv)));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYDBL", PS_DATA_F64,
+                         "psF64 Item", (double)(1.0/(double)(1+lcv)));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYBOOL", PS_DATA_BOOL,
+                         "psBool Item", (lcv%2 == 0));
+            psMetadataAdd(hdu->header, PS_LIST_TAIL, "MYSTR", PS_DATA_STRING,
+
+            bool rc = pmConfigFileRead(&hdu->format, "data/camera0/format0.config", "Camera 0 Config Format");
+            ok(rc == true, "pmConfigFileRead() was successful");
+            rc = pmHDUWrite(hdu, fitsFileW);
+            ok(rc == true, "pmHDUWrite() successfully wrote the header");
+            psFree(hdu);
+        }
+        psFitsClose(fitsFileW);
+
+        psFits* fitsFileR = psFitsOpen(fitsFilename, "r");
+        ok(fitsFileR != NULL, "psFitsOpen returned non-NULL on existing file");
+        int numHDUs = psFitsGetSize(fitsFileR);
+        ok(numHDUs == NUM_HDUS, "The test FITS file has %d HDUs", numHDUs);
+
+        for (int hdunum = 0; hdunum < NUM_HDUS; hdunum++)
+        {
+            char extname[80];
+            snprintf(extname,80, "ext-%d", hdunum);
+            psFitsMoveExtNum(fitsFileR, hdunum, false);
+            pmHDU *hdu = pmHDUAlloc(extname);
+            bool rc = pmHDUReadHeader(hdu, fitsFileR);
+            ok(rc == true, "pmHDUReadHeader() returned TRUE");
+            ok(hdu->header != NULL && psMemCheckMetadata(hdu->header),
+              "pmHDUReadHeader() correctly returned the hdu->header member");
+
+            psS32 intItem = psMetadataLookupS32(NULL, hdu->header, "MYINT");
+            ok(intItem == hdunum, "Retrieved psS32 metadata item from file");
+
+            psF32 fltItem = psMetadataLookupF32(NULL, hdu->header, "MYFLT");
+            ok(fabsf(fltItem - 1.0f/(float)(1+hdunum)) <= FLT_EPSILON,
+               "Retrieved psF32 metadata item from file.  Got %f vs %f",
+               fltItem,1.0f/(float)(1+hdunum));
+
+            psF64 dblItem = psMetadataLookupF64(NULL, hdu->header, "MYDBL");
+            ok(abs(dblItem - 1.0/(double)(1+hdunum)) <= DBL_EPSILON,
+               "Retrieved psF64 metadata item from file.  Got %g vs %g",
+               dblItem, 1.0/(double)(1+hdunum));
+
+            psMetadataItem* boolItem = psMetadataLookup(hdu->header, "MYBOOL");
+            ok(boolItem != NULL && boolItem->type == PS_DATA_BOOL,
+               "Retrieved psBool metadata item from file");
+
+            psString strItem = psMetadataLookupStr(NULL, hdu->header, "MYSTR");
+            ok(strItem != NULL && strncmp(strItem,extname,strlen(extname)) == 0,
+               "Retrieved string metadata item from file.  Got '%s' vs '%s' (%d)",
+               strItem,extname,strlen(extname));
+
+            psFree(hdu);
+        }
+        psFitsClose(fitsFileR);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branch_20071023/psModules/test/camera/tap_pmHDUUtils.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tap_pmHDUUtils.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tap_pmHDUUtils.c	(revision 15358)
@@ -0,0 +1,525 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+// XXX: Use better name for the temporary FITS file
+// XXX: The code to generate and free the FPA hierarchy was copied from
+// tap-pmFPA.c.  EIther include it directly, or library, or something.
+// Also, get rid of the manual free functions and use psFree() once
+// it correctly frees child members
+// XXX: This simpleFPA() requires that the HDUs be allocated.
+
+
+#define CHIP_ALLOC_NAME        "ChipName"
+#define CELL_ALLOC_NAME        "CellName"
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define MISC_NAME2             "META01"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           4
+#define TEST_NUM_COLS           4
+#define NUM_READOUTS            3
+#define NUM_CELLS               10
+#define NUM_CHIPS               8
+#define NUM_HDUS                5
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    chip->hdu = pmHDUAlloc("chipExtName");
+
+    bool rc = pmConfigFileRead(&chip->hdu->format, "data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleChip())");
+    }
+
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip);
+    }
+
+    // XXX: Add code to initialize chip pmConcepts
+
+
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    fpa->hdu = pmHDUAlloc("fpaExtName");
+
+    bool rc = pmConfigFileRead(&fpa->hdu->format, "data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+    }
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa);
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+//    bool mdok;
+//    psMetadata *fileData = psMetadataLookupMetadata(&mdok, fpa->hdu->format, "FILE");
+//    char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+//    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, NULL, fpaNameHdr);
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(73);
+
+    // ----------------------------------------------------------------------
+    // pmHDU *pmHDUFromFPA(const pmFPA *fpa)
+    // pmHDUFromFPA() tests
+    // pmHDUFromFPA() verify with NULL input params
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUFromFPA(NULL);
+        ok(hdu == NULL, "pmHDUFromFPA(NULL) returned NULL");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    //pmHDUFromFPA() verify with acceptable input params
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmHDU *hdu = pmHDUFromFPA(fpa);
+        ok(hdu == fpa->hdu, "pmHDUFromFPA(NULL) returned the correct pmHDU of an pmFPA struct");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmHDU *pmHDUFromChip(const pmChip *chip)
+    // pmHDUFromChip() tests
+    // pmHDUFromChip() verify with NULL input params
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUFromChip(NULL);
+        ok(hdu == NULL, "pmHDUFromChip(NULL) returned NULL");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    //pmHDUFromChip() verify with acceptable input params
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmHDU *hdu = pmHDUFromChip(chip);
+        ok(hdu == chip->hdu, "pmHDUFromChip() returned the correct pmHDU of an pmChip struct");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    //pmHDUFromChip() verify with acceptable input params
+    // Set chip->hdu to NULL, verify chip->parent->hdu is returned
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        psFree(chip->hdu);
+        chip->hdu = NULL;
+        pmHDU *hdu = pmHDUFromChip(chip);
+        ok(hdu == chip->parent->hdu, "pmHDUFromChip() returned the correct pmHDU of an pmChip struct");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmHDU *pmHDUFromCell(const pmCell *cell)
+    // pmHDUFromCell() tests
+    // pmHDUFromCell() verify with NULL input params
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUFromCell(NULL);
+        ok(hdu == NULL, "pmHDUFromCell(NULL) returned NULL");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    //pmHDUFromCell() verify with acceptable input params
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmHDU *hdu = pmHDUFromCell(cell);
+        ok(hdu == cell->hdu, "pmHDUFromCell() returned the correct pmHDU of an pmCell struct");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    //pmHDUFromCell() verify with acceptable input params
+    // Set cell->hdu to NULL, verify cell->parent->hdu is returned
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        psFree(cell->hdu);
+        cell->hdu = NULL;
+        pmHDU *hdu = pmHDUFromCell(cell);
+        ok(hdu == cell->parent->hdu, "pmHDUFromCell() returned the correct pmHDU of an pmCell struct");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmHDU *pmHDUFromReadout(const pmCell *cell)
+    // pmHDUFromReadout() tests
+    // pmHDUFromReadout() verify with NULL input params
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUFromReadout(NULL);
+        ok(hdu == NULL, "pmHDUFromReadout(NULL) returned NULL");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    //pmHDUFromReadout() verify with acceptable input params
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        pmReadout *readout = cell->readouts->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        ok(readout != NULL, "Allocated a pmReadout successfully");
+        pmHDU *hdu = pmHDUFromReadout(readout);
+        ok(hdu == readout->parent->hdu, "pmHDUFromReadout() returned the correct pmHDU of an pmReadout struct");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    //pmHDUGetLowest() tests
+    //pmHDU *pmHDUGetLowest(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+    //pmHDUGetLowest(): verify with all NULL inputs
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUGetLowest(NULL, NULL, NULL);
+        ok(hdu == NULL, "pmHDUFromReadout(NULL, NULL, NULL)");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    //pmHDUGetLowest() tests
+    //pmHDU *pmHDUGetLowest(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+    //pmHDUGetLowest(): verify with all acceptable inputs
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell);
+        ok(hdu == cell->hdu, "pmHDUGetLowest(fpa, chip, cell)");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    //pmHDUGetLowest() tests
+    //pmHDU *pmHDUGetLowest(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+    //pmHDUGetLowest(): verify with all (fpa, chip, NULL) inputs
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmHDU *hdu = pmHDUGetLowest(fpa, chip, NULL);
+        ok(hdu == chip->hdu, "pmHDUGetLowest(fpa, chip, NULL)");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    //pmHDUGetLowest() tests
+    //pmHDU *pmHDUGetLowest(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+    //pmHDUGetLowest(): verify with all (fpa, NULL, NULL) inputs
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmHDU *hdu = pmHDUGetLowest(fpa, NULL, NULL);
+        ok(hdu == fpa->hdu, "pmHDUGetLowest(fpa, NULL, NULL)");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    //pmHDUGetHighest() tests
+    //pmHDU *pmHDUGetHighest(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+    //pmHDUGetHighest(): verify with all NULL inputs
+    {
+        psMemId id = psMemGetId();
+        pmHDU *hdu = pmHDUGetHighest(NULL, NULL, NULL);
+        ok(hdu == NULL, "pmHDUFromReadout(NULL, NULL, NULL)");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    //pmHDUGetHighest() tests
+    //pmHDU *pmHDUGetHighest(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+    //pmHDUGetHighest(): verify with all acceptable inputs
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmHDU *hdu = pmHDUGetHighest(fpa, chip, cell);
+        ok(hdu == fpa->hdu, "pmHDUGetHighest(fpa, chip, cell)");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    //pmHDUGetHighest() tests
+    //pmHDU *pmHDUGetHighest(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+    //pmHDUGetHighest(): verify with (NULL, chip, cell) inputs
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmHDU *hdu = pmHDUGetHighest(NULL, chip, cell);
+        ok(hdu == chip->hdu, "pmHDUGetHighest(NULL, chip, cell)");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    //pmHDUGetHighest() tests
+    //pmHDU *pmHDUGetHighest(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+    //pmHDUGetHighest(): verify with (NULL, NULL, cell) inputs
+    {
+        psMemId id = psMemGetId();
+        // Generate the pmFPA heirarchy
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA* fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        ok(fpa != NULL, "Allocated a pmFPA successfully");
+        ok(chip != NULL, "Allocated a pmChip successfully");
+        ok(cell != NULL, "Allocated a pmCell successfully");
+        pmHDU *hdu = pmHDUGetHighest(NULL, NULL, cell);
+        ok(hdu == cell->hdu, "pmHDUGetHighest(NULL, NULL, cell)");
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+}
Index: /branches/eam_branch_20071023/psModules/test/camera/tst_pmAstrometry.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/camera/tst_pmAstrometry.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/camera/tst_pmAstrometry.c	(revision 15358)
@@ -0,0 +1,615 @@
+/** @file  tst_pmAstrometry.c
+ *
+ *  @brief Contains the tests: pmAstrometry.[ch].  The pmxxxAlloc()
+ *  and psFree() functionality are used here.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  XXX: Untested: pmFPACheckParents()
+ *  XXX: Create the pmHDU alloc/free function, test them here
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-05-01 01:58:04 $
+ *
+ *  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#include "config.h"
+#include <math.h>
+#include <string.h>
+#include "psTest.h"
+#include "pslib_strict.h"
+#include "pmAstrometry.h"
+
+static psS32 testFPAAlloc(void);
+static psS32 testChipAlloc(void);
+static psS32 testCellAlloc(void);
+static psS32 testReadoutAlloc(void);
+
+testDescription tests[] = {
+                              {testFPAAlloc,739,"pmFPAAlloc",0,false},
+                              {testChipAlloc,740,"pmChipAlloc",0,false},
+                              {testCellAlloc,741,"pmCellAlloc",0,false},
+                              {testReadoutAlloc,742,"pmReadoutAlloc",0,false},
+                              {NULL}
+                          };
+
+#define CHIP_ALLOC_NAME "ChipName"
+#define CELL_ALLOC_NAME "CellName"
+#define MISC_NUM 32
+#define MISC_NAME "META00"
+#define MISC_NAME2 "META01"
+#define NUM_BIAS_DATA 10
+#define TEST_NUM_ROWS 32
+#define TEST_NUM_COLS 32
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmFPA *generateSimpleFPA()
+{
+    psBool rc;
+    pmFPA* fpa = pmFPAAlloc(psMetadataAlloc());
+
+    if (fpa == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc() returned a NULL.");
+        return(NULL);
+    }
+
+    //
+    // Test and create camera metadata.
+    //
+    if (fpa->camera == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: fpa->camera is NULL.");
+        psFree(fpa);
+        return(NULL);
+    } else {
+        rc = psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+        if (rc == false) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not add metadata to fpa->camera.");
+            psFree(fpa);
+            return(NULL);
+        }
+        psS32 tmpS32 = psMetadataLookupS32(&rc, fpa->camera, MISC_NAME);
+        if ((rc == false) || (tmpS32 != MISC_NUM)) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not read metadata from fpa->camera.");
+            psFree(fpa);
+            return(NULL);
+        }
+    }
+
+    //
+    // Create various transforms and projections.
+    //
+    fpa->fromTangentPlane = PS_CREATE_4D_IDENTITY_PLANE_DISTORT();
+    fpa->toTangentPlane = PS_CREATE_4D_IDENTITY_PLANE_DISTORT();
+    fpa->projection = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+
+    //
+    // Ensure fpa concepts metadata was allocated properly.
+    //
+    if (fpa->concepts == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set fpa->concepts.");
+        psFree(fpa);
+        return(NULL);
+    } else {
+        rc = psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+        if (rc == false) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not add data to fpa->concepts.");
+            psFree(fpa);
+            return(NULL);
+        }
+        psS32 tmpS32 = psMetadataLookupS32(&rc, fpa->concepts, MISC_NAME);
+        if ((rc == false) || (tmpS32 != MISC_NUM)) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not read metadata from fpa->concepts.");
+            psFree(fpa);
+            return(NULL);
+        }
+    }
+
+    //
+    // Create ->analysis metadata.
+    //
+    fpa->analysis = psMetadataAlloc();
+    rc = psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+
+    //
+    // We test the fpa->chips array later.
+    //
+
+    //
+    // How to test the p_pmHDU *hdu member?
+    //
+
+    //
+    // Create ->phu metadata.
+    //
+    fpa->phu = psMetadataAlloc();
+    rc = psMetadataAddS32(fpa->phu, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+
+    return(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetLevel(PS_LOG_INFO);
+    psLogSetFormat("HLNM");
+
+    return ! runTestSuite(stderr,"pmAstrometry",tests,argc,argv);
+}
+
+/******************************************************************************
+testFPAAlloc()
+    1: We ensure that pmFPAAlloc() properly allocates a pmFPA struct.
+    2: We populate the members with real data to ensure they are being
+       free'ed correctly.
+ *****************************************************************************/
+static psS32 testFPAAlloc(void)
+{
+    psMetadata *camera = psMetadataAlloc();
+    pmFPA* fpa = pmFPAAlloc(camera);
+
+    if (fpa == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc() returned a NULL.");
+        return 1;
+    }
+
+    if (fpa->fromTangentPlane != NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->fromTangentPlane to NULL.");
+        return 2;
+    }
+
+    if (fpa->toTangentPlane != NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->toTangentPlane to NULL.");
+        return 3;
+    }
+    if (fpa->projection != NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->projection to NULL.");
+        return 4;
+    }
+
+    if (fpa->concepts == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->concepts.");
+        return 5;
+    }
+
+    if (fpa->analysis != NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->analysis to NULL.");
+        return 6;
+    }
+
+    if (fpa->camera != camera) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->camera.");
+        return 7;
+    }
+
+    if (fpa->chips == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->chips.");
+        return 8;
+    }
+
+    if (fpa->hdu != NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc did not set ->hdu to NULL.");
+        return 9;
+    }
+    psFree(fpa);
+
+    //
+    // Populate the pmFPA struct with real data to ensure they were
+    // psFree()'ed correctly.
+    //
+    fpa = generateSimpleFPA();
+    if (fpa == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: generateSimpleFPA() returned NULL.");
+        return(15);
+    }
+    psFree(fpa);
+
+    return(0);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    psBool rc;
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    if (chip == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmChipAlloc returned a NULL.");
+        return(NULL);
+    }
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    //
+    // We already ensured that chip->concepts was working properly.
+    //
+
+    //
+    // Create ->analysis metadata.
+    //
+    chip->analysis = psMetadataAlloc();
+    rc = psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+
+    //
+    // We test the chip->cells array later.
+    //
+
+    //
+    // How to test the p_pmHDU *hdu member?
+    //
+
+    return(chip);
+}
+
+static psS32 testChipAlloc(void)
+{
+    pmFPA* fpa = generateSimpleFPA();
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    if (chip == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmChipAlloc returned a NULL.");
+        return 1;
+    }
+
+    if (chip->col0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->col0 set improperly.\n");
+        return 5;
+    }
+
+    if (chip->row0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->row0 set improperly.\n");
+        return 6;
+    }
+
+    if (chip->toFPA != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->toChip set improperly.\n");
+        return 7;
+    }
+
+    if (chip->fromFPA != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->toFPA set improperly.\n");
+        return 8;
+    }
+
+    if (chip->concepts == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->concepts set improperly.\n");
+        return 21;
+    } else {
+        psMetadataItem *tmpMeta = psMetadataLookup(chip->concepts, "CHIP.NAME");
+        if (0 != strcmp((char *) tmpMeta->data.V, CHIP_ALLOC_NAME)) {
+            psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: The metadata was set improperly.\n");
+            return (32);
+        }
+        // XXX: Code a test to ensure the metadata has the correct type
+    }
+
+    if (chip->analysis != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->analysis set improperly.\n");
+        return 10;
+    }
+
+    if (chip->cells == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->cells set improperly.\n");
+        return 22;
+    }
+
+    if (chip->parent != fpa) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->parent set improperly.\n");
+        return 23;
+    }
+
+    if (chip->valid != false) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->valid set improperly.\n");
+        return 24;
+    }
+
+    if (chip->hdu != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: chip->hdu set improperly.\n");
+        return 25;
+    }
+    psFree(fpa);
+
+    //
+    // Populate the pmChip struct with real data to ensure they were
+    // psFree()'ed correctly.
+    //
+    fpa = generateSimpleFPA();
+    chip = generateSimpleChip(fpa);
+    psFree(fpa);
+
+    return(0);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmFPA *fpa, pmChip *chip)
+{
+    psBool rc;
+    pmCell *cell = pmCellAlloc(chip, (psMetadata *) fpa->camera, CELL_ALLOC_NAME);
+    if (cell == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmCellAlloc returned a NULL.");
+        return(NULL);
+    }
+    cell->toChip = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    cell->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    cell->toSky = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    //
+    // We already ensured that cell->concepts was working properly.
+    //
+
+    //
+    // Test camera metadata.
+    //
+    if (cell->camera == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->camera is NULL.");
+        psFree(fpa);
+        return(NULL);
+    } else {
+        rc = psMetadataAddS32((psMetadata *) cell->camera, PS_LIST_HEAD, MISC_NAME2, 0, NULL, MISC_NUM);
+        if (rc == false) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not add metadata to cell->camera.");
+            psFree(fpa);
+            return(NULL);
+        }
+        psS32 tmpS32 = psMetadataLookupS32(&rc, cell->camera, MISC_NAME2);
+        if ((rc == false) || (tmpS32 != MISC_NUM)) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not read metadata from cell->camera.");
+            psFree(fpa);
+            return(NULL);
+        }
+    }
+
+    //
+    // Create ->analysis metadata.
+    //
+    cell->analysis = psMetadataAlloc();
+    rc = psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+
+    //
+    // We test the cell->readouts array later.
+    //
+
+    //
+    // How to test the p_pmHDU *hdu member?
+    //
+
+    return(cell);
+}
+
+static psS32 testCellAlloc(void)
+{
+    pmFPA* fpa = generateSimpleFPA();
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    pmCell *cell = pmCellAlloc(chip, (psMetadata *) fpa->camera, CELL_ALLOC_NAME);
+    if (cell == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmCellAlloc returned a NULL.n");
+        return 3;
+    }
+
+    if (cell->col0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->col0 set improperly.\n");
+        return 5;
+    }
+
+    if (cell->row0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->row0 set improperly.\n");
+        return 6;
+    }
+
+    if (cell->toChip != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->toChip set improperly.\n");
+        return 7;
+    }
+
+    if (cell->toFPA != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->toFPA set improperly.\n");
+        return 8;
+    }
+
+    if (cell->toSky != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->toSky set improperly.\n");
+        return 9;
+    }
+
+    if (cell->concepts == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->concepts set improperly.\n");
+        return 21;
+    } else {
+        psMetadataItem *tmpMeta = psMetadataLookup(cell->concepts, "CELL.NAME");
+        if (0 != strcmp((char *) tmpMeta->data.V, CELL_ALLOC_NAME)) {
+            psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: The metadata was set improperly.\n");
+            return (32);
+        }
+        // XXX: Code a test to ensure the metadata has the correct type
+    }
+
+    if (cell->camera != fpa->camera) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->camera set improperly.\n");
+        return 20;
+    }
+
+    if (cell->analysis != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->analysis set improperly.\n");
+        return 10;
+    }
+
+    if (cell->readouts == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->readouts set improperly.\n");
+        return 22;
+    }
+
+    if (cell->parent != chip) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->parent set improperly.\n");
+        return 23;
+    }
+
+    if (cell->valid != false) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->valid set improperly.\n");
+        return 24;
+    }
+
+    if (cell->hdu != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: cell->hdu set improperly.\n");
+        return 27;
+    }
+    psFree(fpa);
+
+    //
+    // Populate the pmCell struct with real data to ensure they were
+    // psFree()'ed correctly.
+    //
+    fpa = generateSimpleFPA();
+    chip = generateSimpleChip(fpa);
+    cell = generateSimpleCell(fpa, chip);
+    psFree(fpa);
+
+    return(0);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.  We do this to ensure that the data is
+later being psFree()'ed correctly.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    psBool rc;
+    pmReadout *readout = pmReadoutAlloc(cell);
+    if (readout == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmReadoutAlloc returned a NULL.");
+        return(NULL);
+    }
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+
+    //
+    // Create a psList of bias data.
+    //
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        if (readout->bias == NULL) {
+            readout->bias = psListAlloc(tmpImage);
+        } else {
+            psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        }
+    }
+
+    //
+    // Test readout->analysis metadata.
+    //
+    if (readout->analysis == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: readout->analysis is NULL.");
+        psFree(fpa);
+        return(NULL);
+    } else {
+        rc = psMetadataAddS32((psMetadata *) readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+        if (rc == false) {
+            psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: could not add metadata to readout->analysis.");
+            psFree(fpa);
+            return(NULL);
+        }
+    }
+
+    return(readout);
+}
+
+
+static psS32 testReadoutAlloc(void)
+{
+    pmFPA* fpa = generateSimpleFPA();
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    pmCell *cell = pmCellAlloc(chip, (psMetadata *) fpa->camera, CELL_ALLOC_NAME);
+    pmReadout *readout = pmReadoutAlloc(cell);
+    if (readout == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmReadoutAlloc returned a NULL.\n");
+        return 4;
+    }
+
+    if (readout->col0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->col0 set improperly.\n");
+        return 5;
+    }
+
+    if (readout->row0 != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->row0 set improperly.\n");
+        return 6;
+    }
+
+    if (readout->colBins != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->colBins set improperly.\n");
+        return 7;
+    }
+
+    if (readout->rowBins != -1) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->colBins set improperly.\n");
+        return 8;
+    }
+
+    if (readout->image != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->image set improperly.\n");
+        return 10;
+    }
+
+    if (readout->mask != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->mask set improperly.\n");
+        return 12;
+    }
+
+    if (readout->weight != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->weight set improperly.\n");
+        return 14;
+    }
+
+    if (readout->bias != NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->bias set improperly.\n");
+        return 16;
+    }
+
+    if (readout->analysis == NULL) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->analysis set improperly.\n");
+        return 18;
+    }
+
+    if (readout->parent != cell) {
+        psLogMsg(__func__, PS_LOG_ERROR, "TEST ERROR: pmReadout->parent set improperly.\n");
+        return 20;
+    }
+    psFree(fpa);
+
+    //
+    // Populate the pmReadout struct with real data to ensure they were
+    // psFree()'ed correctly.
+    //
+    fpa = generateSimpleFPA();
+    chip = generateSimpleChip(fpa);
+    cell = generateSimpleCell(fpa, chip);
+    readout = generateSimpleReadout(fpa, chip, cell);
+    psFree(fpa);
+
+    return(0);
+}
Index: /branches/eam_branch_20071023/psModules/test/concepts/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/concepts/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/concepts/.cvsignore	(revision 15358)
@@ -0,0 +1,4 @@
+.deps
+.libs
+Makefile
+Makefile.in
Index: /branches/eam_branch_20071023/psModules/test/concepts/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/concepts/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/concepts/Makefile.am	(revision 15358)
@@ -0,0 +1,29 @@
+AM_CPPFLAGS = \
+	$(SRCINC) \
+	-I$(top_srcdir)/test/tap/src \
+	-I$(top_srcdir)/test/pstap/src \
+	$(PSMODULES_CFLAGS)
+
+AM_LDFLAGS = \
+	$(top_builddir)/src/libpsmodules.la  \
+	$(top_builddir)/test/tap/src/libtap.la \
+	$(top_builddir)/test/pstap/src/libpstap.la \
+	$(PSMODULES_LIBS)
+
+TEST_PROGS = \
+	tap_pmConcepts \
+	tap_pmConceptsUpdate \
+	tap_pmConceptsPhotcode \
+	tap_pmConceptsAverage
+
+if BUILD_TESTS
+bin_PROGRAMS = $(TEST_PROGS)
+TESTS = $(TEST_PROGS)
+else
+check_PROGRAMS = $(TEST_PROGS)
+endif
+
+CLEANFILES = $(check_DATA) temp/* core core.* *~ *.bb *.bbg *.da gmon.out
+
+test: check
+	$(top_srcdir)/test/test.pl
Index: /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConcepts.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConcepts.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConcepts.c	(revision 15358)
@@ -0,0 +1,499 @@
+    /** @file tst_pmConcepts.c
+ *
+ *  @brief Contains the tests for pmConcepts.c:
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-18 18:59:30 $
+*   pmConceptSpecAlloc()
+    pmConceptsList()
+*   pmConceptGetRequired()
+*   pmConceptSetRequired()
+    pmConceptRegister()
+    pmConceptsRead()
+*   pmConceptsBlankFPA()
+    pmConceptsReadFPA()
+    pmConceptsWriteFPA()
+*   pmConceptsBlankChip()
+    pmConceptsReadChip()
+    pmConceptsWriteChip()
+*   pmConceptsBlankCell()
+    pmConceptsReadCell()
+    pmConceptsWriteCell()
+*   pmConceptsInit()
+*   pmConceptsDone()
+    pmFPACopyConcepts()
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psMetadataItem *dummyConceptParser(
+    const psMetadataItem *concept,
+    const psMetadataItem *pattern,
+    pmConceptSource source,
+    const psMetadata *cameraFormat,
+    const pmFPA *fpa,
+    const pmChip *chip,
+    const pmCell *cell)
+{
+    if (concept == NULL ||
+        pattern == NULL ||
+        source == PM_CONCEPT_SOURCE_NONE ||
+        cameraFormat == NULL ||
+        fpa == NULL ||
+        chip == NULL ||
+        cell == NULL) {
+        printf("dummyConceptParser() args are NULL\n");
+    }
+    return(NULL);
+}
+
+// FPA.RA and FPA.DEC
+psMetadataItem *dummyConceptFormatter(
+    const psMetadataItem *concept,
+    pmConceptSource source,
+    const psMetadata *cameraFormat,
+    const pmFPA *fpa,
+    const pmChip *chip,
+    const pmCell *cell)
+{
+    if (concept == NULL ||
+        source == PM_CONCEPT_SOURCE_NONE ||
+        cameraFormat == NULL ||
+        fpa == NULL ||
+        chip == NULL ||
+        cell == NULL) {
+        printf("dummyConceptFormatter() args are NULL\n");
+    }
+    return(NULL);
+}
+
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel(".", ERR_TRACE_LEVEL);
+    plan_tests(10);
+    
+    // --------------------------------------------------------------------
+    // Tests for pmConceptSpecAlloc()
+    // Acceptable input parameters.
+    {
+        psMemId id = psMemGetId();
+        psMetadataItem *blank = psMetadataItemAlloc("myItem1", PS_DATA_BOOL, "I am a boolean", true);
+        pmConceptSpec *tmp = pmConceptSpecAlloc(blank, dummyConceptParser,
+            dummyConceptFormatter, true);
+        ok(tmp != NULL, "pmConceptSpecAlloc() returned non-NULL");
+        skip_start(tmp == NULL, 4, "Skipping tests because pmConceptSpecAlloc() returned NULL");
+        ok(tmp->blank == blank, "pmConceptSpecAlloc() set the ->blank member correctly");
+        ok(tmp->parse == dummyConceptParser, "pmConceptSpecAlloc() set the ->parse member correctly");
+        ok(tmp->format == dummyConceptFormatter, "pmConceptSpecAlloc() set the ->format member correctly");
+        ok(tmp->required == true, "pmConceptSpecAlloc() set the ->required member correctly");
+        skip_end();
+        psFree(tmp);
+        psFree(blank);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // NULL input parameters.
+    {
+        psMemId id = psMemGetId();
+        pmConceptSpec *tmp = pmConceptSpecAlloc(NULL, NULL, NULL, false);
+        ok(tmp != NULL, "pmConceptSpecAlloc() returned non-NULL with NULL inputs");
+        psFree(tmp);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // --------------------------------------------------------------------
+    // Tests for pmConceptGetRequired() and pmConceptSetRequired()
+    // Acceptable input parameters.
+    // We get the "required" for FPA.TELESCOPE, ensure that it is false
+    // then set it to true, then ensure that it is true.
+    {
+        psMemId id = psMemGetId();
+        bool tmpBool = pmConceptGetRequired("FPA.TELESCOPE", PM_FPA_LEVEL_FPA);
+        ok (tmpBool == false, "pmConceptGetRequired() returned true for FPA.TELESCOPE");
+        tmpBool = pmConceptSetRequired("FPA.TELESCOPE", PM_FPA_LEVEL_FPA, true);
+        ok (tmpBool == true, "pmConceptSetRequired() returned true for FPA.TELESCOPE");
+        tmpBool = pmConceptGetRequired("FPA.TELESCOPE", PM_FPA_LEVEL_FPA);
+        ok (tmpBool == true, "pmConceptGetRequired() returned true for FPA.TELESCOPE");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Test pmConceptGetRequired() with a few incorrect concept names
+    // and/or levels.
+    {
+        psMemId id = psMemGetId();
+        bool tmpBool = pmConceptGetRequired("FPA.TELESCOPE", PM_FPA_LEVEL_CHIP);
+        ok (tmpBool == false, "pmConceptGetRequired() returned false for FPA.TELESCOPE with wrong level (CHIP)");
+        tmpBool = pmConceptGetRequired("FPA.TELESCOPE", PM_FPA_LEVEL_CELL);
+        ok (tmpBool == false, "pmConceptGetRequired() returned false for FPA.TELESCOPE with wrong level (CELL)");
+        tmpBool = pmConceptGetRequired("FPA.BADCONCEPTNAME", PM_FPA_LEVEL_FPA);
+        ok (tmpBool == false, "pmConceptGetRequired() returned false for FPA.BADCONCEPTNAME");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // --------------------------------------------------------------------
+    // Tests for pmConceptsInit(), pmConceptsDone()
+    // We determine if pmConceptsInit() was successful by looking at the
+    // "required" for the CHIP.XPARITY concept at level PM_FPA_LEVEL_CHIP
+    {
+        psMemId id = psMemGetId();
+        pmConceptsDone();
+        // pmConceptsInit() should return TRUE after pmConceptsDone() is called
+        ok(true == pmConceptsInit(), "pmConceptsInit() returned TRUE");
+
+        // FPA concepts
+        ok(false == pmConceptGetRequired("FPA.TELESCOPE", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.TELESCOPE at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.INSTRUMENT", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.INSTRUMENT at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.DETECTOR", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.DETECTOR at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.CAMERA", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.CAMERA at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.FOCUS", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.FOCUS at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.AIRMASS", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.AIRMASS at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.FILTERID", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.FILTERID at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.FILTER", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.FILTER at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.POSANGLE", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.POSANGLE at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.RADECSYS", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.RADECSYS at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.RA", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.RA at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.DEC", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.DEC at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.OBSTYPE", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.OBSTYPE at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.OBJECT", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.OBJECT at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.ALT", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.ALT at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.AZ", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.AZ at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.TIMESYS", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.TIMESYS at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.TIME", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.TIME at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.TEMP", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.TEMP at level: FPA");
+        ok(false == pmConceptGetRequired("FPA.EXPOSURE", PM_FPA_LEVEL_FPA),
+          "pmConceptGetRequired() returned false for FPA.EXPOSURE at level: FPA");
+
+        // Chip concepts
+        ok(true == pmConceptGetRequired("CHIP.XPARITY", PM_FPA_LEVEL_CHIP),
+          "pmConceptGetRequired() returned true for CHIP.XPARITY at level: CHIP");
+        ok(true == pmConceptGetRequired("CHIP.YPARITY", PM_FPA_LEVEL_CHIP),
+          "pmConceptGetRequired() returned true for CHIP.YPARITY at level: CHIP");
+        ok(true == pmConceptGetRequired("CHIP.X0", PM_FPA_LEVEL_CHIP),
+          "pmConceptGetRequired() returned true for CHIP.X0 at level: CHIP");
+        ok(true == pmConceptGetRequired("CHIP.Y0", PM_FPA_LEVEL_CHIP),
+          "pmConceptGetRequired() returned true for CHIP.Y0 at level: CHIP");
+        ok(true == pmConceptGetRequired("CHIP.XSIZE", PM_FPA_LEVEL_CHIP),
+          "pmConceptGetRequired() returned true for CHIP.XSIZE at level: CHIP");
+        ok(true == pmConceptGetRequired("CHIP.YSIZE", PM_FPA_LEVEL_CHIP),
+          "pmConceptGetRequired() returned true for CHIP.YSIZE at level: CHIP");
+        ok(false == pmConceptGetRequired("CHIP.TEMP", PM_FPA_LEVEL_CHIP),
+          "pmConceptGetRequired() returned true for CHIP.TEMP at level: CHIP");
+
+        // Cell concepts
+        ok(true == pmConceptGetRequired("CELL.GAIN", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.GAIN at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.READNOISE", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.READNOISE at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.SATURATION", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.SATURATION at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.BAD", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.BAD at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.XPARITY", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.XPARITY at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.YPARITY", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.YPARITY at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.READDIR", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.READDIR at level: CELL");
+        ok(false == pmConceptGetRequired("CELL.EXPOSURE", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned false for CELL.EXPOSURE at level: CELL");
+        ok(false == pmConceptGetRequired("CELL.DARKTIME", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned false for CELL.DARKTIME at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.TRIMSEC", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.TRIMSEC at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.BIASSEC", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.BIASSEC at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.XBIN", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.XBIN at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.YBIN", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.YBIN at level: CELL");
+        ok(false == pmConceptGetRequired("CELL.TIMESYS", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned false for CELL.TIMESYS at level: CELL");
+        ok(false == pmConceptGetRequired("CELL.TIME", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned false for CELL.TIME at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.X0", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.X0 at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.Y0", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.Y0 at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.XSIZE", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.XSIZE at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.YSIZE", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.YSIZE at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.XWINDOW", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.XWINDOW at level: CELL");
+        ok(true == pmConceptGetRequired("CELL.YWINDOW", PM_FPA_LEVEL_CELL),
+          "pmConceptGetRequired() returned true for CELL.YWINDOW at level: CELL");
+
+        // The 2nd pmConceptsInit() should return FALSE after pmConceptsInit() is called
+        ok(false == pmConceptsInit(), "pmConceptsInit() returned FALSE");
+
+        pmConceptsDone();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // --------------------------------------------------------------------
+    // Tests for pmConceptsBlankFPA()
+    // Verify error with NULL input.
+    {
+        psMemId id = psMemGetId();
+        bool rc = pmConceptsBlankFPA(NULL);
+        ok(rc == false, "pmConceptsBlankFPA() returned FALSE with NULL input");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // -----------------------------------------------------------------------------
+    // Tests for pmConceptsBlankFPA()
+    // Call with valid data.  We test by ensuring the first 5 metadata items were
+    // added to fpa->concepts.
+    {
+        psMemId id = psMemGetId();
+        bool mdok;
+        char *tmpStr;
+        psF32 tmpF32;
+        psF64 tmpF64;
+        psS32 tmpS32;
+        pmFPA *fpa = pmFPAAlloc(NULL);
+        bool rc = false;
+
+        // First junk items to fpa->concepts so that we know they are later blanked.
+        rc|= psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.TELESCOPE", PS_META_REPLACE, "", "JUNK");
+        rc|= psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.INSTRUMENT", PS_META_REPLACE, "", "JUNK");
+        rc|= psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.DETECTOR", PS_META_REPLACE, "", "JUNK");
+        rc|= psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.CAMERA", PS_META_REPLACE, "", "JUNK");
+        rc|= psMetadataAddF32(fpa->concepts, PS_LIST_TAIL, "FPA.FOCUS", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddF32(fpa->concepts, PS_LIST_TAIL, "FPA.AIRMASS", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.FILTERID", PS_META_REPLACE, "", "JUNK");
+        rc|= psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.FILTER", PS_META_REPLACE, "", "JUNK");
+        rc|= psMetadataAddF32(fpa->concepts, PS_LIST_TAIL, "FPA.POSANGLE", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.RADECSYS", PS_META_REPLACE, "", "JUNK");
+        rc|= psMetadataAddF64(fpa->concepts, PS_LIST_TAIL, "FPA.RA", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddF64(fpa->concepts, PS_LIST_TAIL, "FPA.DEC", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.OBSTYPE", PS_META_REPLACE, "", "JUNK");
+        rc|= psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.OBJECT", PS_META_REPLACE, "", "JUNK");
+        rc|= psMetadataAddF64(fpa->concepts, PS_LIST_TAIL, "FPA.ALT", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddF64(fpa->concepts, PS_LIST_TAIL, "FPA.AZ", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddS32(fpa->concepts, PS_LIST_TAIL, "FPA.TIMESYS", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddF32(fpa->concepts, PS_LIST_TAIL, "FPA.TEMP", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddF32(fpa->concepts, PS_LIST_TAIL, "FPA.EXPOSURE", PS_META_REPLACE, "", 22.0);
+        ok(rc, "Set dummy data in fpa->concepts");
+
+        rc = pmConceptsBlankFPA(fpa);
+        ok(rc == true, "pmConceptsBlankFPA() returned TRUE with valid input data");
+        tmpStr = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.TELESCOPE");
+        ok(mdok && !strcmp(tmpStr, ""), "FPA.TELESCOPE was cleared (%s)", tmpStr);
+        tmpStr = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.INSTRUMENT");
+        ok(mdok && !strcmp(tmpStr, ""), "FPA.INSTRUMENT was cleared (%s)", tmpStr);
+        tmpStr = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.DETECTOR");
+        ok(mdok && !strcmp(tmpStr, ""), "FPA.DETECTOR was cleared (%s)", tmpStr);
+        tmpStr = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.CAMERA");
+        ok(mdok && !strcmp(tmpStr, ""), "FPA.CAMERA was cleared (%s)", tmpStr);
+        tmpF32 = psMetadataLookupF32(&mdok, fpa->concepts, "FPA.FOCUS");
+        ok(mdok && isnan(tmpF32), "FPA.FOCUS was cleared (%f)", tmpF32);
+        tmpF32 = psMetadataLookupF32(&mdok, fpa->concepts, "FPA.AIRMASS");
+        ok(mdok && isnan(tmpF32), "FPA.AIRMASS was cleared (%f)", tmpF32);
+        tmpStr = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.FILTERID");
+        ok(mdok && !strcmp(tmpStr, ""), "FPA.FILTERID was cleared (%s)", tmpStr);
+        tmpStr = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.FILTER");
+        ok(mdok && !strcmp(tmpStr, ""), "FPA.FILTER was cleared (%s)", tmpStr);
+        tmpF32 = psMetadataLookupF32(&mdok, fpa->concepts, "FPA.POSANGLE");
+        ok(mdok && isnan(tmpF32), "FPA.POSANGLE was cleared (%f)", tmpF32);
+        tmpStr = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.RADECSYS");
+        ok(mdok && !strcmp(tmpStr, ""), "FPA.RADECSYS was cleared (%s)", tmpStr);
+        tmpF64 = psMetadataLookupF64(&mdok, fpa->concepts, "FPA.RA");
+        ok(mdok && isnan(tmpF64), "FPA.RA was cleared (%f)", tmpF64);
+        tmpF64 = psMetadataLookupF64(&mdok, fpa->concepts, "FPA.DEC");
+        ok(mdok && isnan(tmpF64), "FPA.DEC was cleared (%f)", tmpF64);
+        tmpStr = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.OBSTYPE");
+        ok(mdok && !strcmp(tmpStr, ""), "FPA.OBSTYPE was cleared (%s)", tmpStr);
+        tmpStr = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.OBJECT");
+        ok(mdok && !strcmp(tmpStr, ""), "FPA.OBJECT was cleared (%s)", tmpStr);
+        tmpF64 = psMetadataLookupF64(&mdok, fpa->concepts, "FPA.ALT");
+        ok(mdok && isnan(tmpF64), "FPA.ALT was cleared (%f)", tmpF64);
+        tmpF64 = psMetadataLookupF64(&mdok, fpa->concepts, "FPA.AZ");
+        ok(mdok && isnan(tmpF64), "FPA.AZ was cleared (%f)", tmpF64);
+        tmpS32 = psMetadataLookupS32(&mdok, fpa->concepts, "FPA.TIMESYS");
+        ok(mdok && -1 == tmpS32, "FPA.TIMESYS was cleared (%d)", tmpS32);
+        // XXX: Add code to make sure it was cleared.
+        psMetadataItem *tmpMI = psMetadataLookup(fpa->concepts, "FPA.TIME");
+        ok(tmpMI != NULL, "FPA.TIME was cleared");
+        tmpF32 = psMetadataLookupF32(&mdok, fpa->concepts, "FPA.TEMP");
+        ok(mdok && isnan(tmpF32), "FPA.TEMP was cleared (%f)", tmpF32);
+        tmpF32 = psMetadataLookupF32(&mdok, fpa->concepts, "FPA.TEMP");
+        ok(mdok && isnan(tmpF32), "FPA.TEMP was cleared (%f)", tmpF32);
+
+        psFree(fpa);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // -----------------------------------------------------------------------------
+    // Tests for pmConceptsBlankChip()
+    // Call with valid data.  We test by ensuring the first 5 metadata items were
+    // added to chip->concepts.
+    {
+        psMemId id = psMemGetId();
+        bool mdok;
+        psF32 tmpF32;
+        psS32 tmpS32;
+        pmChip *chip = pmChipAlloc(NULL, NULL);
+        bool rc = false;
+
+        // First junk items to chip->concepts so that we know they are later blanked.
+        rc|= psMetadataAddS32(chip->concepts, PS_LIST_TAIL, "CHIP.XPARITY", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(chip->concepts, PS_LIST_TAIL, "CHIP.YPARITY", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(chip->concepts, PS_LIST_TAIL, "CHIP.X0", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(chip->concepts, PS_LIST_TAIL, "CHIP.Y0", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(chip->concepts, PS_LIST_TAIL, "CHIP.XSIZE", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(chip->concepts, PS_LIST_TAIL, "CHIP.YSIZE", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddF32(chip->concepts, PS_LIST_TAIL, "CHIP.TEMP", PS_META_REPLACE, "", 22.0);
+        ok(rc, "Set dummy data in chip->concepts");
+
+        rc = pmConceptsBlankChip(chip);
+        tmpS32 = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.XPARITY");
+        ok(mdok && 0 == tmpS32, "CHIP.XPARITY was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.YPARITY");
+        ok(mdok && 0 == tmpS32, "CHIP.YPARITY was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.X0");
+        ok(mdok && 0 == tmpS32, "CHIP.X0 was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.Y0");
+        ok(mdok && 0 == tmpS32, "CHIP.Y0 was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.XSIZE");
+        ok(mdok && 0 == tmpS32, "CHIP.XSIZE was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, chip->concepts, "CHIP.YSIZE");
+        ok(mdok && 0 == tmpS32, "CHIP.YSIZE was cleared (%d)", tmpS32);
+        tmpF32 = psMetadataLookupF32(&mdok, chip->concepts, "CHIP.TEMP");
+        ok(mdok && isnan(tmpF32), "CHIP.TEMP was cleared (%d)", tmpF32);
+
+        psFree(chip);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // -----------------------------------------------------------------------------
+    // Tests for pmConceptsBlankCell()
+    // Call with valid data.  We test by ensuring the first 5 metadata items were
+    // added to cell->concepts.
+    {
+        psMemId id = psMemGetId();
+        bool mdok;
+        psF32 tmpF32;
+        psS32 tmpS32;
+        pmCell *cell = pmCellAlloc(NULL, NULL);
+        bool rc = false;
+
+        // First junk items to cell->concepts so that we know they are later blanked.
+        rc|= psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.GAIN", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.READNOISE", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.XPARITY", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.YPARITY", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.READDIR", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.SATURATION", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.BAD", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.EXPOSURE", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.DARKTIME", PS_META_REPLACE, "", 22.0);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.XBIN", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.YBIN", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.TIMESYS", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.X0", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.Y0", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.XSIZE", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.YSIZE", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.XWINDOW", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.YWINDOW", PS_META_REPLACE, "", 22);
+        rc|= psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.", PS_META_REPLACE, "", 22);
+
+
+        ok(rc, "Set dummy data in cell->concepts");
+
+        rc = pmConceptsBlankCell(cell);
+        ok(rc == true, "pmConceptsBlankCELL() returned TRUE with valid input data");
+
+        tmpF32 = psMetadataLookupF32(&mdok, cell->concepts, "CELL.GAIN");
+        ok(mdok && isnan(tmpF32), "CELL.GAIN was cleared (%f)", tmpF32);
+        tmpF32 = psMetadataLookupF32(&mdok, cell->concepts, "CELL.READNOISE");
+        ok(mdok && isnan(tmpF32), "CELL.READNOISE was cleared (%f)", tmpF32);
+        tmpF32 = psMetadataLookupF32(&mdok, cell->concepts, "CELL.SATURATION");
+        ok(mdok && isnan(tmpF32), "CELL.SATURATION was cleared (%f)", tmpF32);
+        tmpF32 = psMetadataLookupF32(&mdok, cell->concepts, "CELL.BAD");
+        ok(mdok && isnan(tmpF32), "CELL.BAD was cleared (%f)", tmpF32);
+
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.XPARITY");
+        ok(mdok && 0 == tmpS32, "CELL.XPARITY was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.YPARITY");
+        ok(mdok && 0 == tmpS32, "CELL.YPARITY was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.READDIR");
+        ok(mdok && 0 == tmpS32, "CELL.READDIR was cleared (%d)", tmpS32);
+        tmpF32 = psMetadataLookupF32(&mdok, cell->concepts, "CELL.EXPOSURE");
+        ok(mdok && isnan(tmpF32), "CELL.EXPOSURE was cleared (%f)", tmpF32);
+        tmpF32 = psMetadataLookupF32(&mdok, cell->concepts, "CELL.DARKTIME");
+        ok(mdok && isnan(tmpF32), "CELL.DARKTIME was cleared (%f)", tmpF32);
+        // XXX: Add code to make sure it was cleared.
+        psMetadataItem *tmpMI = psMetadataLookup(cell->concepts, "CELL.TRIMSEC");
+        ok(tmpMI != NULL, "CELL.TRIMSEC was cleared");
+        tmpMI = psMetadataLookup(cell->concepts, "CELL.BIASSEC");
+        ok(tmpMI != NULL, "CELL.BIASSEC was cleared");
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.XBIN");
+        ok(mdok && 0 == tmpS32, "CELL.XBIN was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.YBIN");
+        ok(mdok && 0 == tmpS32, "CELL.YBIN was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.TIMESYS");
+        ok(mdok && -1 == tmpS32, "CELL.TIMESYS was cleared (%d)", tmpS32);
+        // XXX: Add code to make sure it was cleared.
+        tmpMI = psMetadataLookup(cell->concepts, "CELL.TIME");
+        ok(tmpMI != NULL, "CELL.TIME was cleared");
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.X0");
+        ok(mdok && 0 == tmpS32, "CELL.X0 was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.Y0");
+        ok(mdok && 0 == tmpS32, "CELL.Y0 was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.XSIZE");
+        ok(mdok && 0 == tmpS32, "CELL.XSIZE was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.YSIZE");
+        ok(mdok && 0 == tmpS32, "CELL.YSIZE was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.XWINDOW");
+        ok(mdok && 0 == tmpS32, "CELL.XWINDOW was cleared (%d)", tmpS32);
+        tmpS32 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.YWINDOW");
+        ok(mdok && 0 == tmpS32, "CELL.YWINDOW was cleared (%d)", tmpS32);
+
+        psFree(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+}
Index: /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConceptsAverage.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConceptsAverage.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConceptsAverage.c	(revision 15358)
@@ -0,0 +1,486 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+// XXX: Use better name for the temporary FITS file
+// XXX: The code to generate and free the FPA hierarchy was copied from
+// tap-pmFPA.c.  EIther include it directly, or library, or something.
+// Also, get rid of the manual free functions and use psFree() once
+// it correctly frees child members
+// XXX: For the genSimpleFPA() code, add IDs to each function so that
+// the values set in each chip-?cell-?hdu-?image are unique
+// XXX: For the genSimpleFPA() code, write masks and weights as well
+
+#define CHIP_ALLOC_NAME        "ChipName"
+#define CELL_ALLOC_NAME        "CellName"
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define MISC_NAME2             "META01"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           4
+#define TEST_NUM_COLS           4
+#define NUM_READOUTS            3
+#define NUM_CELLS               10
+#define NUM_CHIPS               8
+#define NUM_FPAS		4
+#define NUM_HDUS                5
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../camera/data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    chip->hdu = pmHDUAlloc("chipExtName");
+//
+//    bool rc = pmConfigFileRead(&chip->hdu->format, "../camera/data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleChip())");
+//    }
+//
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip);
+    }
+
+    // XXX: Add code to initialize chip pmConcepts
+
+
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    fpa->hdu = pmHDUAlloc("fpaExtName");
+//
+//    bool rc = pmConfigFileRead(&fpa->hdu->format, "../camera/data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+//    }
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa);
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+//    bool mdok;
+//    psMetadata *fileData = psMetadataLookupMetadata(&mdok, fpa->hdu->format, "FILE");
+//    char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+//    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, NULL, fpaNameHdr);
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(50);
+
+    // ----------------------------------------------------------------------
+    // pmConceptsAverageFPAs() tests: NULL input pmFPA *target
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *targetFPA = generateSimpleFPA(camera);
+        pmFPA *sourceFPA[NUM_FPAS];
+
+        sourceFPA[0] = generateSimpleFPA(camera);
+        psList *sources = psListAlloc(sourceFPA[0]);
+        for (int fpaID = 1 ; fpaID < NUM_FPAS ; fpaID++) {
+            sourceFPA[fpaID] = generateSimpleFPA(camera);
+            bool rc = psListAdd(sources, PS_LIST_HEAD, sourceFPA[fpaID]);
+            ok(rc, "Successfully added FPA %d to list", fpaID);
+	}
+        ok(!pmConceptsAverageFPAs(NULL, sources), "pmConceptsAverage(NULL, sources) returned FALSE");
+
+        for (int fpaID = 0 ; fpaID < NUM_FPAS ; fpaID++) {
+            myFreeFPA(sourceFPA[fpaID]);
+	}
+        psFree(sources);
+        myFreeFPA(targetFPA);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmConceptsAverageFPAs() tests: NULL input psList *sources
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *targetFPA = generateSimpleFPA(camera);
+        pmFPA *sourceFPA[NUM_FPAS];
+
+        sourceFPA[0] = generateSimpleFPA(camera);
+        psList *sources = psListAlloc(sourceFPA[0]);
+        for (int fpaID = 1 ; fpaID < NUM_FPAS ; fpaID++) {
+            sourceFPA[fpaID] = generateSimpleFPA(camera);
+            bool rc = psListAdd(sources, PS_LIST_HEAD, sourceFPA[fpaID]);
+            ok(rc, "Successfully added FPA %d to list", fpaID);
+	}
+        ok(!pmConceptsAverageFPAs(targetFPA, NULL), "pmConceptsAverage(NULL, sources) returned FALSE");
+
+        for (int fpaID = 0 ; fpaID < NUM_FPAS ; fpaID++) {
+            myFreeFPA(sourceFPA[fpaID]);
+	}
+        psFree(sources);
+        myFreeFPA(targetFPA);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmConceptsAverageFPAs() tests: acceptable inputs
+    // XXX: There's a memory leak somewhere in this test, not sure where.
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *targetFPA = generateSimpleFPA(camera);
+        pmFPA *sourceFPA[NUM_FPAS];
+
+        // Ensure that the FPA.TIME average is computed correctly
+        psList *sources = NULL;
+        psF64 actualTime = 0.0;
+        for (int fpaID = 0 ; fpaID < NUM_FPAS ; fpaID++) {
+            sourceFPA[fpaID] = generateSimpleFPA(camera);
+            psTime *fpaTime = psMetadataLookupPtr(NULL, (sourceFPA[fpaID])->concepts, "FPA.TIME");
+            // Add a small value to the psTime so that we can test/ensure that pmConceptsAverageFPAs()
+            // is actually calculating an average.
+            fpaTime->sec += (double) (fpaID * 1000);
+            actualTime+= psTimeToMJD(fpaTime);
+            if (0 == fpaID) {
+                sources = psListAlloc(sourceFPA[fpaID]);
+	    } else {
+                bool rc = psListAdd(sources, PS_LIST_HEAD, sourceFPA[fpaID]);
+                ok(rc, "Successfully added FPA %d to list", fpaID);
+	    }
+	}
+        ok(pmConceptsAverageFPAs(targetFPA, sources), "pmConceptsAverage(NULL, sources) returned FALSE");
+        actualTime/= (float) NUM_FPAS;
+        psTime *fpaTime = psMetadataLookupPtr(NULL, targetFPA->concepts, "FPA.TIME");
+        ok(abs(actualTime - psTimeToMJD(fpaTime)) < 1e-4, "pmConceptsAverageFPAs() calculated the average time correctly");
+
+
+        // Replace the FPA.TIMESYS with a non-conforming value, verify that pmConceptsAverageFPAs() returnes an error        
+        psTimeType timeSys = psMetadataLookupS32(NULL, sourceFPA[0]->concepts, "FPA.TIMESYS");
+        psMetadataAddS32(sourceFPA[NUM_FPAS-1]->concepts, PS_LIST_HEAD, "FPA.TIMESYS", PS_META_REPLACE, NULL, timeSys+10);
+        ok(!pmConceptsAverageFPAs(targetFPA, sources), "pmConceptsAverage(NULL, sources) returned FALSE with nonequal FPA.TIMESYS metadata");
+        psMetadataAddS32(sourceFPA[NUM_FPAS-1]->concepts, PS_LIST_HEAD, "FPA.TIMESYS", PS_META_REPLACE, NULL, timeSys);
+        ok(pmConceptsAverageFPAs(targetFPA, sources), "pmConceptsAverage(NULL, sources) returned TRUE with equal FPA.TIMESYS metadata");
+
+
+        // Replace the FPA.TIMESYS with a non-conforming value, verify that pmConceptsAverageFPAs() returnes an error        
+        psString filter = psMetadataLookupStr(NULL, sourceFPA[0]->concepts, "FPA.FILTER");
+        psMetadataAddStr(sourceFPA[NUM_FPAS-1]->concepts, PS_LIST_HEAD, "FPA.FILTER", PS_META_REPLACE, NULL, "BOGUS STRING");
+        ok(!pmConceptsAverageFPAs(targetFPA, sources), "pmConceptsAverage(NULL, sources) returned FALSE with nonequal FPA.FILTER metadata");
+        psMetadataAddStr(sourceFPA[NUM_FPAS-1]->concepts, PS_LIST_HEAD, "FPA.FILTER", PS_META_REPLACE, NULL, filter);
+        ok(pmConceptsAverageFPAs(targetFPA, sources), "pmConceptsAverage(NULL, sources) returned TRUE with equal FPA.FILTER metadata");
+
+        // Free data, check for memory leaks
+        for (int fpaID = 0 ; fpaID < NUM_FPAS ; fpaID++) {
+            myFreeFPA(sourceFPA[fpaID]);
+	}
+        psFree(sources);
+        myFreeFPA(targetFPA);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // ----------------------------------------------------------------------
+    // pmConceptsAverageCells() tests: NULL input pmFPA *target
+    // bool pmConceptsAverageCells(pmCell *target, psList *sources, psRegion *trimsec, psRegion *biassec, bool same)
+    {
+        psMemId id = psMemGetId();
+        psMetadata *tgtCamera = psMetadataAlloc();
+        pmFPA *tgtFPA = generateSimpleFPA(tgtCamera);
+        pmChip *tgtChip = tgtFPA->chips->data[0];
+        pmCell *tgtCell = tgtChip->cells->data[0];
+        psMetadata *srcCamera = psMetadataAlloc();
+        pmFPA *srcFPA = generateSimpleFPA(srcCamera);
+        pmChip *srcChip = srcFPA->chips->data[0];
+
+        psList *sources = NULL;
+        psF32 tstGain = 0.0;
+        psF32 tstReadnoise = 0.0;
+        psF32 tstExposure = 0.0;
+        psF32 tstDarktime = 0.0;
+        psF32 tstSaturation = 0.0;
+        psF32 tstBad = 0.0;
+        
+        for (int cellID = 0 ; cellID < srcChip->cells->n ; cellID++) {
+            pmCell *cell = srcChip->cells->data[cellID];
+            // Set the various concepts which we will test later
+            psMetadataAddF32(cell->concepts, PS_LIST_HEAD, "CELL.GAIN", PS_META_REPLACE, NULL, 0.0 + (float) cellID);
+            tstGain+= 0.0 + (float) cellID;
+            psMetadataAddF32(cell->concepts, PS_LIST_HEAD, "CELL.READNOISE", PS_META_REPLACE, NULL, 10.0 + (float) cellID);
+            tstReadnoise+= 10.0 + (float) cellID;
+            psMetadataAddF32(cell->concepts, PS_LIST_HEAD, "CELL.EXPOSURE", PS_META_REPLACE, NULL, 20.0 + (float) cellID);
+            tstExposure+= 20.0 + (float) cellID;
+            psMetadataAddF32(cell->concepts, PS_LIST_HEAD, "CELL.DARKTIME", PS_META_REPLACE, NULL, 30.0 + (float) cellID);
+            tstDarktime+= 30.0 + (float) cellID;
+            psMetadataAddF32(cell->concepts, PS_LIST_HEAD, "CELL.SATURATION", PS_META_REPLACE, NULL, 40.0 + (float) cellID);
+            if (cellID == 0)
+                tstSaturation = 40.0 + (float) cellID;
+            psMetadataAddF32(cell->concepts, PS_LIST_HEAD, "CELL.BAD", PS_META_REPLACE, NULL, 50.0 + (float) cellID);
+            if (cellID == (srcChip->cells->n - 1))
+                tstBad = 50.0 + (float) cellID;
+            if (cellID == 0) {
+                sources = psListAlloc(srcChip->cells->data[cellID]);
+	    } else {
+                bool rc = psListAdd(sources, PS_LIST_HEAD, srcChip->cells->data[cellID]);
+                ok(rc, "Successfully added cell %d to list", cellID);
+	    }
+	}
+        tstGain /= (psF64) srcChip->cells->n;
+        tstReadnoise /= (psF64) srcChip->cells->n;
+        tstExposure /= (psF64) srcChip->cells->n;
+        tstDarktime /= (psF64) srcChip->cells->n;
+
+        psRegion *trimsec = psRegionAlloc(0, 1, 2, 3);
+        psRegion *biassec = psRegionAlloc(4, 5, 6, 7);
+
+        // Ensure pmConceptsAverageCells() returns NULL with NULL tgtCell input
+        bool rc = pmConceptsAverageCells(NULL, sources, trimsec, biassec, false);
+        ok(!rc, "pmConceptsAverageCells() returned FALSE with NULL sources input");
+
+        // Ensure pmConceptsAverageCells() returns NULL with NULL sources input
+        rc = pmConceptsAverageCells(tgtCell, NULL, trimsec, biassec, false);
+        ok(!rc, "pmConceptsAverageCells() returned FALSE with NULL targetCell input");
+
+        // Ensure pmConceptsAverageCells() returns NULL with sources->n = 0
+        sources->n = 0;
+        rc = pmConceptsAverageCells(tgtCell, sources, trimsec, biassec, false);
+        ok(!rc, "pmConceptsAverageCells() returned FALSE with NULL sources->n = 0");
+        sources->n = NUM_CELLS;
+
+        // Call pmConceptsAverageCells() with acceptable input data
+        rc = pmConceptsAverageCells(tgtCell, sources, trimsec, biassec, false);
+        ok(rc, "pmConceptsAverageCells() returned TRUE with acceptable input data");
+        psF32 tmpF32;
+        tmpF32 = psMetadataLookupF32(NULL, tgtCell->concepts, "CELL.GAIN");
+        ok(abs(tmpF32 - tstGain) < 1e-4, "pmConceptsAverageCells() calculated the average CELL.GAIN correctly");
+        tmpF32 = psMetadataLookupF32(NULL, tgtCell->concepts, "CELL.READNOISE");
+        ok(abs(tmpF32 - tstReadnoise) < 1e-4, "pmConceptsAverageCells() calculated the average CELL.READNOISE correctly");
+        tmpF32 = psMetadataLookupF32(NULL, tgtCell->concepts, "CELL.EXPOSURE");
+        ok(abs(tmpF32 - tstExposure) < 1e-4, "pmConceptsAverageCells() calculated the average CELL.EXPOSURE correctly");
+        tmpF32 = psMetadataLookupF32(NULL, tgtCell->concepts, "CELL.DARKTIME");
+        ok(abs(tmpF32 - tstDarktime) < 1e-4, "pmConceptsAverageCells() calculated the average CELL.DARKTIME correctly");
+        tmpF32 = psMetadataLookupF32(NULL, tgtCell->concepts, "CELL.SATURATION");
+        ok(abs(tmpF32 - tstSaturation) < 1e-4, "pmConceptsAverageCells() calculated the average CELL.SATURATION correctly (%f %f)", tmpF32, tstSaturation);
+        tmpF32 = psMetadataLookupF32(NULL, tgtCell->concepts, "CELL.BAD");
+        ok(abs(tmpF32 - tstBad) < 1e-4, "pmConceptsAverageCells() calculated the average CELL.BAD correctly");
+        psRegion *tstTrimsec = psMetadataLookupPtr(NULL, tgtCell->concepts, "CELL.TRIMSEC");
+        ok(tstTrimsec->x0 == 0.0 && tstTrimsec->x1 == 1.0 && tstTrimsec->y0 == 2.0 && tstTrimsec->y1 == 3.0,
+           "pmConceptsAverageCells() set the CELL>TRIMSEC region correctly");
+        psRegion *tstBiassec = psMetadataLookupPtr(NULL, tgtCell->concepts, "CELL.BIASSEC");
+        ok(tstBiassec->x0 == 4.0 && tstBiassec->x1 == 5.0 && tstBiassec->y0 == 6.0 && tstBiassec->y1 == 7.0,
+           "pmConceptsAverageCells() set the CELL>TRIMSEC region correctly");
+
+        psS32 tmpS32;
+        pmCell *srcCell = srcChip->cells->data[NUM_CELLS - 1];
+
+        // Set the CELL.TIMESYS metadata unequal, verify that error occurs
+        rc = psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.TIMESYS", PS_META_REPLACE, NULL, 232);
+        tmpS32 = psMetadataLookupS32(&rc, srcCell->concepts, "CELL.TIMESYS");
+        rc = pmConceptsAverageCells(tgtCell, sources, trimsec, biassec, false);
+        ok(!rc, "pmConceptsAverageCells() returned FALSE with non equal CELL.TIMESYS metadata");
+        psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.TIMESYS", PS_META_REPLACE, NULL, tmpS32);
+
+        // Set the CELL.READDIR metadata unequal, verify that error occurs
+        rc = psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.READDIR", PS_META_REPLACE, NULL, 232);
+        tmpS32 = psMetadataLookupS32(&rc, srcCell->concepts, "CELL.READDIR");
+        rc = pmConceptsAverageCells(tgtCell, sources, trimsec, biassec, false);
+        ok(!rc, "pmConceptsAverageCells() returned FALSE with non equal CELL.READDIR metadata");
+        psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.READDIR", PS_META_REPLACE, NULL, tmpS32);
+
+        // Set the CELL.XBIN metadata unequal, verify that error occurs
+        rc = psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.XBIN", PS_META_REPLACE, NULL, 232);
+        tmpS32 = psMetadataLookupS32(&rc, srcCell->concepts, "CELL.XBIN");
+        rc = pmConceptsAverageCells(tgtCell, sources, trimsec, biassec, false);
+        ok(!rc, "pmConceptsAverageCells() returned FALSE with non equal CELL.XBIN metadata");
+        psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.XBIN", PS_META_REPLACE, NULL, tmpS32);
+
+        // Set the CELL.YBIN metadata unequal, verify that error occurs
+        rc = psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.YBIN", PS_META_REPLACE, NULL, 232);
+        tmpS32 = psMetadataLookupS32(&rc, srcCell->concepts, "CELL.YBIN");
+        rc = pmConceptsAverageCells(tgtCell, sources, trimsec, biassec, false);
+        ok(!rc, "pmConceptsAverageCells() returned FALSE with non equal CELL.YBIN metadata");
+        psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.YBIN", PS_META_REPLACE, NULL, tmpS32);
+
+        // Set the CELL.X0 metadata unequal, verify that error occurs
+        rc = psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.X0", PS_META_REPLACE, NULL, 232);
+        tmpS32 = psMetadataLookupS32(&rc, srcCell->concepts, "CELL.X0");
+        rc = pmConceptsAverageCells(tgtCell, sources, trimsec, biassec, true);
+        ok(!rc, "pmConceptsAverageCells() returned FALSE with non equal CELL.X0 metadata (same = true)");
+        psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.X0", PS_META_REPLACE, NULL, tmpS32);
+
+        // Set the CELL.Y0 metadata unequal, verify that error occurs
+        rc = psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.Y0", PS_META_REPLACE, NULL, 232);
+        tmpS32 = psMetadataLookupS32(&rc, srcCell->concepts, "CELL.Y0");
+        rc = pmConceptsAverageCells(tgtCell, sources, trimsec, biassec, true);
+        ok(!rc, "pmConceptsAverageCells() returned FALSE with non equal CELL.Y0 metadata (same = true)");
+        psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.Y0", PS_META_REPLACE, NULL, tmpS32);
+
+        // Set the CELL.X0 metadata unequal, verify that no error occurs with same==false
+        rc = psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.X0", PS_META_REPLACE, NULL, 232);
+        tmpS32 = psMetadataLookupS32(&rc, srcCell->concepts, "CELL.X0");
+        rc = pmConceptsAverageCells(tgtCell, sources, trimsec, biassec, false);
+        ok(!rc, "pmConceptsAverageCells() returned FALSE with non equal CELL.X0 metadata (same = false)");
+        psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.X0", PS_META_REPLACE, NULL, tmpS32);
+
+        // Set the CELL.Y0 metadata unequal, verify that no error occurs with same==false
+        rc = psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.Y0", PS_META_REPLACE, NULL, 232);
+        tmpS32 = psMetadataLookupS32(&rc, srcCell->concepts, "CELL.Y0");
+        rc = pmConceptsAverageCells(tgtCell, sources, trimsec, biassec, false);
+        ok(!rc, "pmConceptsAverageCells() returned FALSE with non equal CELL.Y0 metadata (same = false)");
+        psMetadataAddS32(srcCell->concepts, PS_LIST_HEAD, "CELL.Y0", PS_META_REPLACE, NULL, tmpS32);
+
+        myFreeFPA(tgtFPA);
+        myFreeFPA(srcFPA);
+        psFree(tgtCamera);
+        psFree(srcCamera);
+        psFree(biassec);
+        psFree(trimsec);
+        psFree(sources);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+}
+
+
Index: /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConceptsPhotcode.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConceptsPhotcode.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConceptsPhotcode.c	(revision 15358)
@@ -0,0 +1,114 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(25);
+
+
+    // ----------------------------------------------------------------------
+    // pmConceptsPhotcodeForView() tests: NULL pmConfig input
+    // psString pmConceptsPhotcodeForView(pmConfig *config, pmFPAfile *file, const pmFPAview *view)
+    {
+        psMemId id = psMemGetId();
+        pmConfig *config =pmConfigAlloc();
+        pmFPAfile *file = pmFPAfileAlloc();
+        pmFPAview *view = pmFPAviewAlloc(32);
+        ok(NULL == pmConceptsPhotcodeForView(NULL, file, view),
+          "pmConceptsPhotcodeForView(NULL, file, view) returned NULL");
+        psFree(config);
+        psFree(file);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmConceptsPhotcodeForView() tests: NULL pmConfig input
+    // psString pmConceptsPhotcodeForView(pmConfig *config, pmFPAfile *file, const pmFPAview *view)
+    {
+        psMemId id = psMemGetId();
+        pmConfig *config =pmConfigAlloc();
+        pmFPAfile *file = pmFPAfileAlloc();
+        pmFPAview *view = pmFPAviewAlloc(32);
+        ok(NULL == pmConceptsPhotcodeForView(NULL, file, view),
+          "pmConceptsPhotcodeForView(NULL, file, view) returned NULL");
+        psFree(config);
+        psFree(file);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmConceptsPhotcodeForView() tests: NULL pmFPAfile input
+    // psString pmConceptsPhotcodeForView(pmConfig *config, pmFPAfile *file, const pmFPAview *view)
+    {
+        psMemId id = psMemGetId();
+        pmConfig *config =pmConfigAlloc();
+        pmFPAfile *file = pmFPAfileAlloc();
+        pmFPAview *view = pmFPAviewAlloc(32);
+        ok(NULL == pmConceptsPhotcodeForView(config, NULL, view),
+          "pmConceptsPhotcodeForView(config, NULL, view) returned NULL");
+        psFree(config);
+        psFree(file);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmConceptsPhotcodeForView() tests: NULL pmFPAview input
+    // psString pmConceptsPhotcodeForView(pmConfig *config, pmFPAfile *file, const pmFPAview *view)
+    {
+        psMemId id = psMemGetId();
+        pmConfig *config =pmConfigAlloc();
+        pmFPAfile *file = pmFPAfileAlloc();
+        pmFPAview *view = pmFPAviewAlloc(32);
+        ok(NULL == pmConceptsPhotcodeForView(config, file, NULL),
+          "pmConceptsPhotcodeForView(config, file, NULL) returned NULL");
+        psFree(config);
+        psFree(file);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // pmConceptsPhotcodeForView() tests: acceptable inputs
+    // psString pmConceptsPhotcodeForView(pmConfig *config, pmFPAfile *file, const pmFPAview *view)
+    {
+        psMemId id = psMemGetId();
+        psString str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "../config/data/SampleIPPConfig";
+        psS32 argc = 3;
+        pmConfig *config = pmConfigRead(&argc, str, "RecipeName");
+        ok(config, "pmConfigRead() returned non-NULL");
+        pmFPAfile *file = pmFPAfileAlloc();
+        // XXX: Insert code to read a pmFPAfile correctly
+        pmFPAview *view = pmFPAviewAlloc(0);
+
+        bool rc;
+        psMetadata *recipe  = psMetadataLookupPtr(&rc, config->recipes, "PPIMAGE");
+        char *rule = psMetadataLookupStr(&rc, recipe, "PHOTCODE.RULE");
+        psString goodPhotcode = pmFPAfileNameFromRule(rule, file, view);
+
+        psString testPhotcode = pmConceptsPhotcodeForView(config, file, view);
+        ok(testPhotcode, "pmConceptsPhotcodeForView(config, file, view) returned non-NULL");
+        ok(!strcmp(goodPhotcode, testPhotcode), "pmConceptsPhotcodeForView() produced the correct string");
+        psFree(config);
+        psFree(file);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
+//HERE
Index: /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConceptsUpdate.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConceptsUpdate.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/concepts/tap_pmConceptsUpdate.c	(revision 15358)
@@ -0,0 +1,290 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+// XXX: Use better name for the temporary FITS file
+// XXX: The code to generate and free the FPA hierarchy was copied from
+// tap-pmFPA.c.  EIther include it directly, or library, or something.
+// Also, get rid of the manual free functions and use psFree() once
+// it correctly frees child members
+// XXX: For the genSimpleFPA() code, add IDs to each function so that
+// the values set in each chip-?cell-?hdu-?image are unique
+// XXX: For the genSimpleFPA() code, write masks and weights as well
+
+#define CHIP_ALLOC_NAME        "ChipName"
+#define CELL_ALLOC_NAME        "CellName"
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define MISC_NAME2             "META01"
+#define NUM_BIAS_DATA           10
+#define TEST_NUM_ROWS           4
+#define TEST_NUM_COLS           4
+#define NUM_READOUTS            3
+#define NUM_CELLS               10
+#define NUM_CHIPS               8
+#define NUM_HDUS                5
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+psPlaneTransform *PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM()
+{
+    psPlaneTransform *pt = psPlaneTransformAlloc(1, 1);
+    pt->x->coeff[1][0] = 1.0;
+    pt->y->coeff[0][1] = 1.0;
+    return(pt);
+}
+
+psPlaneDistort *PS_CREATE_4D_IDENTITY_PLANE_DISTORT()
+{
+    psPlaneDistort *pd = psPlaneDistortAlloc(1, 1, 1, 1);
+    pd->x->coeff[1][0][0][0] = 1.0;
+    pd->y->coeff[0][1][0][0] = 1.0;
+    return(pd);
+}
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../camera/data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+/******************************************************************************
+generateSimpleChip(): This function generates a pmChip data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmChip *generateSimpleChip(pmFPA *fpa)
+{
+    pmChip *chip = pmChipAlloc(fpa, CHIP_ALLOC_NAME);
+    chip->toFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    chip->fromFPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    psMetadataAddS32(chip->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(chip->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    chip->hdu = pmHDUAlloc("chipExtName");
+//
+//    bool rc = pmConfigFileRead(&chip->hdu->format, "../camera/data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleChip())");
+//    }
+//
+    psArrayRealloc(chip->cells, NUM_CELLS);
+    for (int i = 0 ; i < NUM_CELLS ; i++) {
+        chip->cells->data[i] = generateSimpleCell(chip);
+    }
+
+    // XXX: Add code to initialize chip pmConcepts
+
+
+    return(chip);
+}
+
+/******************************************************************************
+generateSimpleFPA(): This function generates a pmFPA data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmFPA* generateSimpleFPA(psMetadata *camera)
+{
+    pmFPA* fpa = pmFPAAlloc(camera);
+    fpa->fromTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toTPA = PS_CREATE_2D_IDENTITY_PLANE_TRANSFORM();
+    fpa->toSky = psProjectionAlloc(0.0,0.0,10.0,10.0,PS_PROJ_TAN);
+    psMetadataAddS32(fpa->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32((psMetadata *) fpa->camera, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(fpa->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+//    fpa->hdu = pmHDUAlloc("fpaExtName");
+//
+//    bool rc = pmConfigFileRead(&fpa->hdu->format, "../camera/data/camera0/format0.config", "Camera format 0");
+//    if (!rc) {
+//        diag("pmConfigFileRead() was unsuccessful (from generateSimpleFPA())");
+//    }
+
+    psArrayRealloc(fpa->chips, NUM_CHIPS);
+    for (int i = 0 ; i < NUM_CHIPS ; i++) {
+        fpa->chips->data[i] = generateSimpleChip(fpa);
+    }
+
+    // XXX: Eventually, when you finish the pmConcepts tests, add full concept
+    // reading code from wherever.
+    pmConceptsBlankFPA(fpa);
+//    bool mdok;
+//    psMetadata *fileData = psMetadataLookupMetadata(&mdok, fpa->hdu->format, "FILE");
+//    char *fpaNameHdr = psMetadataLookupStr(&mdok, fileData, "FPA.NAME");
+//    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, NULL, fpaNameHdr);
+
+    return(fpa);
+}
+
+// XXX: This should only be necessary until the psFree() functions for
+// FPA/chip/cell/readout correctly free all child chips/cells/readouts
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+void myFreeChip(pmChip *chip) {
+    for (int j = 0 ; j < chip->cells->n ; j++) {
+        myFreeCell(chip->cells->data[j]);
+    }
+    psFree(chip);
+}
+
+void myFreeFPA(pmFPA *fpa)
+{
+    for (int i = 0 ; i < fpa->chips->n ; i++) {
+        myFreeChip(fpa->chips->data[i]);
+    }
+    psFree(fpa);
+}
+
+
+psS32 main(psS32 argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(7);
+
+
+    // ----------------------------------------------------------------------
+    // pmConceptsUpdate() tests: NULL inputs
+    // bool pmConceptsUpdate(const pmFPA *fpa, const pmChip *chip, const pmCell *cell)
+    {
+        psMemId id = psMemGetId();
+        ok(pmConceptsUpdate(NULL, NULL, NULL), "pmConceptsUpdate(NULL, NULL, NULL) returned TRUE");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+    
+    // pmConceptsUpdate() tests: acceptable inputs
+    // XXX: Must add tests for the BIASSEC concepts
+    {
+        psMemId id = psMemGetId();
+        psMetadata *camera = psMetadataAlloc();
+        pmFPA *fpa = generateSimpleFPA(camera);
+        pmChip *chip = fpa->chips->data[0];
+        pmCell *cell = chip->cells->data[0];
+        bool rc = pmConceptsUpdate(NULL, NULL, cell);
+
+        // Calculate expected myTrimsec
+        bool xStatus, yStatus; // Status of MD lookups
+        psImageBinning *binning = psImageBinningAlloc();
+        binning->nXbin = psMetadataLookupS32(&xStatus, cell->concepts, "CELL.XBIN");
+        binning->nYbin = psMetadataLookupS32(&yStatus, cell->concepts, "CELL.YBIN");
+        psRegion *goodTrimsec = psMetadataLookupPtr(NULL, cell->concepts, "CELL.TRIMSEC");
+        double goodTrimsecX0 = goodTrimsec->x0;
+        double goodTrimsecX1 = goodTrimsec->x1;
+        double goodTrimsecY0 = goodTrimsec->y0;
+        double goodTrimsecY1 = goodTrimsec->y1;
+        goodTrimsecX0/= binning->nXbin;
+        goodTrimsecX1/= binning->nXbin;
+        goodTrimsecY0/= binning->nYbin;
+        goodTrimsecY1/= binning->nYbin;
+        goodTrimsecX0 = (int) goodTrimsec->x0;
+        if (goodTrimsec->x1 > (int)goodTrimsec->x1) {
+            goodTrimsecX1 = (int)goodTrimsec->x1 + 1;
+         } else {
+            goodTrimsecX1 = (int)goodTrimsec->x1;
+	 }
+        goodTrimsecY0 = (int)goodTrimsec->y0;
+        if (goodTrimsec->y1 > (int)goodTrimsec->y1) {
+            goodTrimsecY1 = (int)goodTrimsec->y1 + 1;
+	} else {
+            goodTrimsecY1 = (int)goodTrimsec->y1;
+	}
+        psFree(binning);
+
+        // Add CELL.TRIMSEC.UPDATE concept to cell->concepts
+        psMetadataAddS32(cell->concepts, PS_LIST_HEAD, "CELL.TRIMSEC.UPDATE", 0, NULL, 32);
+        psS32 num = psMetadataLookupS32(&rc, cell->concepts, "CELL.TRIMSEC.UPDATE");
+        ok(rc, "Successfully added CELL.TRIMSEC.UPDATE to cell->concepts (%d)", num);
+
+        // Add CELL.BIASSEC.UPDATE concept to cell->concepts
+        psMetadataAddS32(cell->concepts, PS_LIST_HEAD, "CELL.BIASSEC.UPDATE", 0, NULL, 32);
+        num = psMetadataLookupS32(&rc, cell->concepts, "CELL.BIASSEC.UPDATE");
+        ok(rc, "Successfully added CELL.BIASSEC.UPDATE to cell->concepts (%d)", num);
+
+        // Call pmConceptsUpdate to update the CELL.TRIMSEC concept
+        ok(rc, "pmConceptsUpdate(NULL, NULL, pmCell) returned TRUE");
+
+        psRegion *testTrimsec = psMetadataLookupPtr(NULL, cell->concepts, "CELL.TRIMSEC"); // Trim section
+        ok(testTrimsec->x1 == goodTrimsecX1 && testTrimsec->y1 == goodTrimsecY1 &&
+           testTrimsec->x0 == goodTrimsecX0 && testTrimsec->y0 == goodTrimsecY0,
+           "pmConceptsUpdate() updated CELL.TRIMSEC correctly (%f %f)",
+            testTrimsec->x1, testTrimsec->y1);
+
+        // XXX: This fails but the problem might be with the test code
+        if (0) {
+            num = psMetadataLookupS32(&rc, cell->concepts, "CELL.TRIMSEC.UPDATE");
+            ok(!rc, "pmConceptsUpdate() removed CELL.TRIMSEC.UPDATE to cell->concepts (%d)", num);
+        }
+
+        myFreeFPA(fpa);
+        psFree(camera);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+}
+
Index: /branches/eam_branch_20071023/psModules/test/config/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/.cvsignore	(revision 15358)
@@ -0,0 +1,7 @@
+.deps
+.libs
+Makefile
+Makefile.in
+temp
+tst_pmConfig
+SampleIPPConfig
Index: /branches/eam_branch_20071023/psModules/test/config/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/Makefile.am	(revision 15358)
@@ -0,0 +1,38 @@
+AM_CPPFLAGS = \
+	$(SRCINC) \
+	-I$(top_srcdir)/test/tap/src \
+	-I$(top_srcdir)/test/pstap/src \
+	$(PSMODULES_CFLAGS)
+
+AM_LDFLAGS = \
+	$(top_builddir)/src/libpsmodules.la  \
+	$(top_builddir)/test/tap/src/libtap.la \
+	$(top_builddir)/test/pstap/src/libpstap.la \
+	$(PSMODULES_LIBS)
+
+TEST_PROGS = \
+	tap_pmConfig \
+	tap_pmConfigCommand \
+	tap_pmConfigMask \
+	tap_pmErrorCodes \
+	tap_pmVersion
+
+check_DATA = \
+    SampleIPPConfig
+
+SampleIPPConfig: data/SampleIPPConfig
+	cp $? $@
+
+EXTRA_DIST = data
+
+if BUILD_TESTS
+bin_PROGRAMS = $(TEST_PROGS)
+TESTS = $(TEST_PROGS)
+else
+check_PROGRAMS = $(TEST_PROGS)
+endif
+
+CLEANFILES = $(check_DATA) temp/* core core.* *~ *.bb *.bbg *.da gmon.out
+
+test: check
+	$(top_srcdir)/test/test.pl
Index: /branches/eam_branch_20071023/psModules/test/config/data/SampleIPPConfig
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/SampleIPPConfig	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/SampleIPPConfig	(revision 15358)
@@ -0,0 +1,74 @@
+### Example .ipprc file
+    PATH            STR     .
+    DATAPATH	str	datapath
+
+PATH            STR     .
+
+### Database configuration
+DBSERVER	STR	localhost		# Database host name (for psDBInit)
+DBUSER		STR	test			# Database user name (for psDBInit)
+DBPASSWORD	STR	DB-PW			# Database password (for psDBInit)
+DBNAME          STR     test                    # ????
+DBPORT		S32	0
+
+
+### Setups for each camera system
+CAMERAS		METADATA
+	CAMERA0		STR	camera0/camera.config
+	CAMERA1		STR	camera1/camera.config
+END
+
+### psLib setup
+#TIME		STR	/home/mithrandir/price/pan-starrs/jhroot/i686-pc-linux-gnu/etc/pslib/psTime.config	# Time configuration file
+LOGLEVEL	S32	3			# Logging level; 3=INFO
+LOGFORMAT	STR	HLNM			# Log format
+LOGDEST	STR	STDOUT				# Log destination
+TRACE		METADATA			# Trace levels
+	dummyTraceFunc01	S32	1
+	dummyTraceFunc02	S32	2
+END
+ARBITRARY_STRING_S32	S32	20
+ARBITRARY_STRING_F32	F32	21.0
+ARBITRARY_STRING_F64	F64	22.0
+ARBITRARY_STRING_STR	STR	19.0
+ARBITRARY_STRING_BOOL_T	BOOL	true
+ARBITRARY_STRING_BOOL_F	BOOL	false
+=======
+    CAMERAS		METADATA
+	CAMERA0		STR	camera0/camera.config
+	CAMERA1		STR	camera1/camera.config
+    END
+
+### Setups for psLib
+    TIME		STR	time.config
+    LOGLEVEL	S32	3
+    LOGFORMAT	STR	HLNM
+    LOGDEST	STR	STDOUT
+
+### Default trace logging initializations
+    TRACE		METADATA
+	dummyTraceFacility01	S32	1
+	dummyTraceFacility02	S32	2
+    END
+
+### Predefined recipe files
+    RECIPES         METADATA                # Site-level recipes
+        R00		STR	recipes/R00.config
+        R01		STR	recipes/R01.config
+        R02		STR	recipes/R02.config
+        R03		STR	recipes/R03.config
+    END
+
+### Misc arbitraty metadata
+    ARBITRARY_STRING_S32	S32	20
+    ARBITRARY_STRING_F32	F32	21.0
+    ARBITRARY_STRING_F64	F64	22.0
+    ARBITRARY_STRING_STR	STR	19.0
+    ARBITRARY_STRING_BOOL_T	BOOL	true
+    ARBITRARY_STRING_BOOL_F	BOOL	false
+
+RECIPES         METADATA
+	MASKS			STR	recipes_masks.config
+        JUNK_RECIPE_00		STR	recipe_file00
+        JUNK_RECIPE_01		STR	recipe_file01
+END
Index: /branches/eam_branch_20071023/psModules/test/config/data/camera0/camera.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/camera0/camera.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/camera0/camera.config	(revision 15358)
@@ -0,0 +1,15 @@
+FORMATS         METADATA
+        C0_FM0     STR     camera0/format0.config
+        C0_FM1     STR     camera0/format1.config
+END
+
+RECIPES         METADATA
+        C0_RECIPE0          STR     camera0/recipe0.config
+        C0_RECIPE1          STR     camera0/recipe1.config
+END
+
+FPA     METADATA
+        ccd00   STR     LeftAmp RightAmp
+        ccd01   STR     LeftAmp RightAmp
+END
+ID      STR     CAMERA0
Index: /branches/eam_branch_20071023/psModules/test/config/data/camera0/format0.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/camera0/format0.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/camera0/format0.config	(revision 15358)
@@ -0,0 +1,36 @@
+RULE    METADATA
+        F0_KEY0        STR     string20
+        F0_KEY1        STR     string21
+        F0_KEY2        S32     20
+END
+
+FILE    METADATA
+        PHU             STR     FPA
+        EXTENSIONS      STR     CELL
+        FPA.NAME        STR     EXPNUM
+END
+
+DEFAULTS        METADATA
+        F0_DEF0           STR     DefString20
+        F0_DEF1           STR     DefString21
+        F0_DEF2           F32     21.0
+        F0_DEF3           S32     22
+END
+
+CELLS   METADATA
+        left    METADATA        # Left amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+        right   METADATA        # Right amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+END
+
+ID	STR	camera0/format0.config
+
+TRANSLATION     METADATA
+        FPA.TELESCOPE   STR     TELESCOP
+        FPA.INSTRUMENT  STR     INSTRUME
+END
Index: /branches/eam_branch_20071023/psModules/test/config/data/camera0/format1.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/camera0/format1.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/camera0/format1.config	(revision 15358)
@@ -0,0 +1,34 @@
+RULE    METADATA
+        F1_KEY0        STR     string30
+        F1_KEY1        STR     string31
+        F1_KEY2        S32     30
+END
+
+FILE    METADATA
+        PHU             STR     FPA
+        EXTENSIONS      STR     CELL
+        FPA.NAME        STR     EXPNUM
+END
+
+DEFAULTS        METADATA
+        F1_DEF0           STR     DefString30
+        F1_DEF1           STR     DefString31
+        F1_DEF2           F32     31.0
+        F1_DEF3           S32     32
+END
+
+CELLS   METADATA
+        left    METADATA        # Left amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+        right   METADATA        # Right amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+END
+
+TRANSLATION     METADATA
+        FPA.TELESCOPE   STR     TELESCOP
+        FPA.INSTRUMENT  STR     INSTRUME
+END
Index: /branches/eam_branch_20071023/psModules/test/config/data/camera0/recipe0.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/camera0/recipe0.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/camera0/recipe0.config	(revision 15358)
@@ -0,0 +1,4 @@
+R0_KEY0		STR	RecipeString0
+R0_KEY1		STR	RecipeString1
+R0_KEY2		S32	2
+R0_KEY3		F32	3
Index: /branches/eam_branch_20071023/psModules/test/config/data/camera0/recipe1.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/camera0/recipe1.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/camera0/recipe1.config	(revision 15358)
@@ -0,0 +1,4 @@
+R1_KEY0		STR	RecipeString10
+R1_KEY1		STR	RecipeString11
+R1_KEY2		S32	12
+R1_KEY3		F32	13
Index: /branches/eam_branch_20071023/psModules/test/config/data/camera1/camera.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/camera1/camera.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/camera1/camera.config	(revision 15358)
@@ -0,0 +1,14 @@
+FORMATS         METADATA
+        C1_FM0     STR     camera1/format0.config
+        C1_FM1     STR     camera1/format1.config
+END
+
+RECIPES         METADATA
+        C1_RECIPE0          STR     camera1/recipe0.config
+        C1_RECIPE1          STR     camera1/recipe1.config
+END
+
+FPA     METADATA
+        ccd00   STR     LeftAmp RightAmp
+        ccd01   STR     LeftAmp RightAmp
+END
Index: /branches/eam_branch_20071023/psModules/test/config/data/camera1/format0.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/camera1/format0.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/camera1/format0.config	(revision 15358)
@@ -0,0 +1,34 @@
+RULE    METADATA
+        F0_KEY0        STR     string40
+        F0_KEY1        STR     string41
+        F0_KEY2        S32     420
+END
+
+FILE    METADATA
+        PHU             STR     FPA
+        EXTENSIONS      STR     CELL
+        FPA.NAME        STR     EXPNUM
+END
+
+DEFAULTS        METADATA
+        F0_DEF0           STR     DefString40
+        F0_DEF1           STR     DefString41
+        F0_DEF2           F32     41.0
+        F0_DEF3           S32     44
+END
+
+CELLS   METADATA
+        left    METADATA        # Left amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+        right   METADATA        # Right amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+END
+
+TRANSLATION     METADATA
+        FPA.TELESCOPE   STR     TELESCOP
+        FPA.INSTRUMENT  STR     INSTRUME
+END
Index: /branches/eam_branch_20071023/psModules/test/config/data/camera1/format1.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/camera1/format1.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/camera1/format1.config	(revision 15358)
@@ -0,0 +1,34 @@
+RULE    METADATA
+        F1_KEY0        STR     string80
+        F1_KEY1        STR     string81
+        F1_KEY2        S32     80
+END
+
+FILE    METADATA
+        PHU             STR     FPA
+        EXTENSIONS      STR     CELL
+        FPA.NAME        STR     EXPNUM
+END
+
+DEFAULTS        METADATA
+        F1_DEF0           STR     DefString80
+        F1_DEF1           STR     DefString81
+        F1_DEF2           F32     81.0
+        F1_DEF3           S32     82
+END
+
+CELLS   METADATA
+        left    METADATA        # Left amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+        right   METADATA        # Right amplifier
+                CELL.BIASSEC.SOURCE     STR     HEADER
+                CELL.TRIMSEC.SOURCE     STR     HEADER
+        END
+END
+
+TRANSLATION     METADATA
+        FPA.TELESCOPE   STR     TELESCOP
+        FPA.INSTRUMENT  STR     INSTRUME
+END
Index: /branches/eam_branch_20071023/psModules/test/config/data/camera1/recipe0.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/camera1/recipe0.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/camera1/recipe0.config	(revision 15358)
@@ -0,0 +1,4 @@
+R0_KEY0		STR	RecipeString120
+R0_KEY1		STR	RecipeString121
+R0_KEY2		S32	122
+R0_KEY3		F32	123
Index: /branches/eam_branch_20071023/psModules/test/config/data/camera1/recipe1.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/camera1/recipe1.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/camera1/recipe1.config	(revision 15358)
@@ -0,0 +1,4 @@
+R1_KEY0		STR	RecipeString230
+R1_KEY1		STR	RecipeString2311
+R1_KEY2		S32	232
+R1_KEY3		F32	233
Index: /branches/eam_branch_20071023/psModules/test/config/data/path2/SampleIPPConfig2
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/path2/SampleIPPConfig2	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/path2/SampleIPPConfig2	(revision 15358)
@@ -0,0 +1,32 @@
+### Example .ipprc file
+
+### Database configuration
+DBSERVER	STR	ippdb.ifa.hawaii.edu	# Database host name (for psDBInit)
+DBUSER		STR	ipp			# Database user name (for psDBInit)
+DBPASSWORD	STR	password		# Database password (for psDBInit)
+
+### Setups for each camera system
+CAMERAS		METADATA
+	MEGACAM_RAW	STR	megacam_raw.config
+	MEGACAM_SPLICE	STR	megacam_splice.config
+	GPC1_RAW	STR	gpc1_raw.config
+	LRIS_BLUE	STR	lris_blue.config
+	LRIS_RED	STR	lris_red.config
+END
+
+### psLib setup
+#TIME		STR	/home/mithrandir/price/pan-starrs/jhroot/i686-pc-linux-gnu/etc/pslib/psTime.config	# Time configuration file
+LOGLEVEL	S32	3			# Logging level; 3=INFO
+LOGFORMAT	STR	HLNM			# Log format
+LOGDEST	STR	STDOUT				# Log destination
+TRACE		METADATA			# Trace levels
+	dummyTraceFunc01	S32	1
+	dummyTraceFunc02	S32	2
+END
+ARBITRARY_STRING_S32	S32	20
+ARBITRARY_STRING_F32	F32	21.0
+ARBITRARY_STRING_F64	F64	22.0
+ARBITRARY_STRING_STR	STR	19.0
+ARBITRARY_STRING_BOOL_T	BOOL	true
+ARBITRARY_STRING_BOOL_F	BOOL	false
+
Index: /branches/eam_branch_20071023/psModules/test/config/data/recipe_file00
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/recipe_file00	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/recipe_file00	(revision 15358)
@@ -0,0 +1,2 @@
+recipe00		BOOL    TRUE
+recipe01		BOOL    FALSE
Index: /branches/eam_branch_20071023/psModules/test/config/data/recipe_file01
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/recipe_file01	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/recipe_file01	(revision 15358)
@@ -0,0 +1,2 @@
+recipe00		BOOL    FALSE
+recipe01		BOOL    TRUE
Index: /branches/eam_branch_20071023/psModules/test/config/data/recipes_masks.config
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/data/recipes_masks.config	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/data/recipes_masks.config	(revision 15358)
@@ -0,0 +1,10 @@
+### Recipe specifying values for various mask concepts
+BLANK           U8      0x01            # The pixel is blank or has no (valid) data
+FLAT            U8      0x02            # The pixel is non-positive in the flat-field
+DETECTOR        U8      0x02            # The detector pixel is bad (e.g., bad column, charge trap)
+SAT             U8      0x04            # The pixel is saturated in the image of interest
+BAD             U8      0x04            # The pixel is low in the image of interest
+RANGE           U8      0x04            # The pixel is out of range in the image of interest
+CR              U8      0x08            # The pixel is probably a CR
+SUSPECT         U8      0x40            # The pixel is suspected of being bad, but may not be
+MARK            U8      0x80            # The pixel is marked as temporarily ignored
Index: /branches/eam_branch_20071023/psModules/test/config/tap_pmConfig.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/tap_pmConfig.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/tap_pmConfig.c	(revision 15358)
@@ -0,0 +1,1212 @@
+/** @file tst_pmConfig.c
+ *
+ *  @brief Contains the tests for pmConfig.c:
+ *
+ * This code will test the pmConfig() routine.
+*       pmConfigReadParamsSet
+*       pmConfigAlloc		
+*       pmConfigSet
+*       pmConfigDone
+*       pmConfigFileRead
+*        pmConfigValidateCameraFormat
+*        pmConfigCameraFormatFromHeader
+*        pmConfigCameraByName
+?        pmConfigDB
+*        pmConfigConformHeader
+-        pmConfigFileSets
+-        pmConfigFileSetsMD
+*        pmConfigConvertFilename
+*        pmConfigRead
+ * XXXX: Must determine what to do with NULL arguments, then test it.
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-18 18:55:36 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+#define ERR_TRACE_LEVEL         0
+#define	VERBOSE			0
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel(".", 0);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(181);
+    
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Tests for pmConfigReadParamsSet()
+    {
+        psMemId id = psMemGetId();
+        bool oldReadCameraConfig = pmConfigReadParamsSet(false);
+        ok(oldReadCameraConfig == true, "pmConfigReadParamsSet() returned old value correctly");
+        oldReadCameraConfig = pmConfigReadParamsSet(true);
+        ok(oldReadCameraConfig == false, "pmConfigReadParamsSet() returned old value correctly");
+        oldReadCameraConfig = pmConfigReadParamsSet(true);
+        ok(oldReadCameraConfig == true, "pmConfigReadParamsSet() returned new value correctly");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+    
+    
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Tests for pmConfigAlloc()
+    // XXX: Add a MemCheckConfig() function to verify the return type
+    {
+        psMemId id = psMemGetId();
+
+        pmConfig *myConfig = pmConfigAlloc();
+        ok(myConfig != NULL, "pmConfigAlloc() returned non-NULL");
+        skip_start(myConfig == NULL, 10, "skipping tests because pmConfigAlloc() returned NULL");
+        ok(myConfig->site == NULL, "pmConfigAlloc() initialized pmConfig->site properly");
+        ok(myConfig->camera == NULL, "pmConfigAlloc() initialized pmConfig->camera properly");
+        ok(myConfig->cameraName == NULL, "pmConfigAlloc() initialized pmConfig->cameraName properly");
+        ok(myConfig->format == NULL, "pmConfigAlloc() initialized pmConfig->format properly");
+        ok(myConfig->formatName == NULL, "pmConfigAlloc() initialized pmConfig->formatName properly");
+        ok(myConfig->recipes != NULL && psMemCheckMetadata(myConfig->recipes),
+            "pmConfigAlloc() initialized pmConfig->recipes properly");
+        ok(myConfig->recipesRead == PM_RECIPE_SOURCE_NONE, "pmConfigAlloc() initialized pmConfig->recipesRead properly");
+        ok(myConfig->recipeSymbols != NULL && psMemCheckMetadata(myConfig->recipeSymbols),
+            "pmConfigAlloc() initialized pmConfig->recipesSymbols properly");
+        ok(myConfig->arguments != NULL && psMemCheckMetadata(myConfig->arguments),
+            "pmConfigAlloc() initialized pmConfig->arguments properly");
+        ok(myConfig->database == NULL, "pmConfigAlloc() initialized pmConfig->database properly");
+        ok(myConfig->defaultRecipe == NULL, "pmConfigAlloc() initialized pmConfig->defaultRecipe properly");
+
+        skip_end();
+        psFree(myConfig);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    
+    
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigSet()
+    // Test with NULL input
+    // XX: Test with empty string?
+    {
+        psMemId id = psMemGetId();
+        pmConfigSet(NULL);
+        ok(true, "called pmConfigSet() with NULL input pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_VALUE == tmpErr->code,
+          "pmConfigSet(NULL) created the PS_ERR_BAD_PARAMETER_VALUE error");
+        psFree(tmpErr);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+    
+    
+    // Test with acceptable config file name, with a simple path
+    {
+        pmConfigDone();
+        psMemId id = psMemGetId();
+        psMetadata *config = NULL;
+        bool rc = pmConfigFileRead(&config, "SampleIPPConfig2", "DESCRIPTION");
+        ok(rc == false, "pmConfigFileRead() returned FALSE before calling pmConfigSet()");
+        pmConfigSet("data/path2");
+        rc = pmConfigFileRead(&config, "SampleIPPConfig2", "DESCRIPTION");
+        ok(rc == true, "pmConfigFileRead() returned TRUE after calling pmConfigSet() (test 1)");
+        psFree(config);
+        pmConfigDone();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with acceptable config file name, with a compound path
+    {
+        pmConfigDone();
+        psMemId id = psMemGetId();
+        psMetadata *config = NULL;
+        bool rc = pmConfigFileRead(&config, "SampleIPPConfig2", "DESCRIPTION");
+        ok(rc == false, "pmConfigFileRead() returned FALSE before calling pmConfigSet()");
+        pmConfigSet("junk:data/path2:data/path5");
+        rc = pmConfigFileRead(&config, "SampleIPPConfig2", "DESCRIPTION");
+        ok(rc == true, "pmConfigFileRead() returned TRUE after calling pmConfigSet() (test 2)");
+        psFree(config);
+        pmConfigDone();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigDone(): ensure it frees memory allocated by pmConfigSet();
+    // This also ensures that pmConfigSet() psFrees memory between calls
+    {
+        psMemId id = psMemGetId();
+        pmConfigSet("junk01");
+        pmConfigSet("junk:data/path2:data/path5");
+        pmConfigSet("junk02");
+        pmConfigDone();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks: pmConfigDone()");
+    }
+
+
+
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigFileRead()
+    // Test with NULL config pointer
+    // XXX: There's a memory leak if the first argument points to data that's
+    // already allocated (pmConfigFileRead() frees the first argument)
+    {
+        psMemId id = psMemGetId();
+        psString name = "foo1";
+        psString desc = "foo2";
+        bool rc = pmConfigFileRead(NULL, name, desc);
+        ok(rc == false, "pmConfigFileRead() returned FALSE with NULL config pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigFileRead() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with NULL name pointer
+    // XXX: Test with empty name string?
+    {
+        psMemId id = psMemGetId();
+        psMetadata *config = psMetadataAlloc();
+        psString desc = "foo2";
+        bool rc = pmConfigFileRead(&config, NULL, desc);
+        ok(rc == false, "pmConfigFileRead() returned FALSE with NULL name pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_VALUE == tmpErr->code,
+          "pmConfigFileRead() created the PS_ERR_BAD_PARAMETER_VALUE error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with NULL desc pointer
+    // XXX: Test with empty desc string and otherwise acceptable inputs.
+    // That generated errors elsewhere.
+    {
+        psMemId id = psMemGetId();
+        psMetadata *config = psMetadataAlloc();
+        psString name = "foo1";
+        bool rc = pmConfigFileRead(&config, name, NULL);
+        ok(rc == false, "pmConfigFileRead() returned FALSE with NULL desc pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_VALUE == tmpErr->code,
+          "pmConfigFileRead() created the PS_ERR_BAD_PARAMETER_VALUE error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with acceptable config file name, no paths
+    // This generates an error because config is non-NULL and pmConfigFileRead()
+    // psFrees it.  Create a bugzilla report.
+    {
+        psMemId id = psMemGetId();
+        psMetadata *config = psMetadataAlloc();
+        psString name = "data/SampleIPPConfig";
+        psString desc = "DESCRIPTION";
+        bool rc = pmConfigFileRead(&config, name, desc);
+        ok(rc == true, "pmConfigFileRead() returned TRUE with acceptable inputs");
+
+        // Test the several arbitrary config file strings.
+        psS32 tmpInt = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_S32");
+        ok(rc == true && tmpInt == 20, "pmConfigFileRead() properly set metadata (S32)");
+        psF32 tmpFloat = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_F32");
+        ok(rc == true && tmpFloat == 21.0, "pmConfigFileRead() properly set metadata (F32)");
+        psF64 tmpDub = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_F64");
+        ok(rc == true && tmpDub == 22.0, "pmConfigFileRead() properly set metadata (F64)");
+        psBool tmpBool = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_BOOL_T");
+        ok(rc == true && tmpBool == true, "pmConfigFileRead() properly set metadata (bool)");
+        tmpBool = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_BOOL_F");
+        ok(rc == true && tmpBool == false, "pmConfigFileRead() properly set metadata (bool)");
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with acceptable config file name, no paths
+    {
+        psMemId id = psMemGetId();
+        psMetadata *config = NULL;
+        psString name = "data/SampleIPPConfig";
+        psString desc = "DESCRIPTION";
+        bool rc = pmConfigFileRead(&config, name, desc);
+        ok(rc == true, "pmConfigFileRead() returned TRUE with acceptable inputs");
+
+        // Test the several arbitrary config file strings.
+        psS32 tmpInt = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_S32");
+        ok(rc == true && tmpInt == 20, "pmConfigFileRead() properly set metadata (S32)");
+        psF32 tmpFloat = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_F32");
+        ok(rc == true && tmpFloat == 21.0, "pmConfigFileRead() properly set metadata (F32)");
+        psF64 tmpDub = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_F64");
+        ok(rc == true && tmpDub == 22.0, "pmConfigFileRead() properly set metadata (F64)");
+        psBool tmpBool = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_BOOL_T");
+        ok(rc == true && tmpBool == true, "pmConfigFileRead() properly set metadata (bool)");
+        tmpBool = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_BOOL_F");
+        ok(rc == true && tmpBool == false, "pmConfigFileRead() properly set metadata (bool)");
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Test with acceptable config file name, no paths
+    {
+        psMemId id = psMemGetId();
+        psMetadata *config = NULL;
+        psString name = "data/SampleIPPConfig";
+        psString desc = "DESCRIPTION";
+        bool rc = pmConfigFileRead(&config, name, desc);
+        ok(rc == true, "pmConfigFileRead() returned TRUE with acceptable inputs");
+
+        // Test the several arbitrary config file strings.
+        psS32 tmpInt = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_S32");
+        ok(rc == true && tmpInt == 20, "pmConfigFileRead() properly set metadata (S32)");
+        psF32 tmpFloat = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_F32");
+        ok(rc == true && tmpFloat == 21.0, "pmConfigFileRead() properly set metadata (F32)");
+        psF64 tmpDub = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_F64");
+        ok(rc == true && tmpDub == 22.0, "pmConfigFileRead() properly set metadata (F64)");
+        psBool tmpBool = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_BOOL_T");
+        ok(rc == true && tmpBool == true, "pmConfigFileRead() properly set metadata (bool)");
+        tmpBool = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_BOOL_F");
+        ok(rc == true && tmpBool == false, "pmConfigFileRead() properly set metadata (bool)");
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Test with acceptable config file name, with path
+    // Note: This also tests the pmConfigSet() function.
+    {
+        psMemId id = psMemGetId();
+        psMetadata *config = NULL;
+        psString name = "SampleIPPConfig2";
+        psString desc = "DESCRIPTION";
+        pmConfigSet("data/path2");
+        bool rc = pmConfigFileRead(&config, name, desc);
+        ok(rc == true, "pmConfigFileRead() returned TRUE using paths");
+        // Test the several arbitrary config file strings.
+        psS32 tmpInt = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_S32");
+        ok(rc == true && tmpInt == 20, "pmConfigFileRead() properly set metadata (S32)");
+        psF32 tmpFloat = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_F32");
+        ok(rc == true && tmpFloat == 21.0, "pmConfigFileRead() properly set metadata (F32)");
+        psF64 tmpDub = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_F64");
+        ok(rc == true && tmpDub == 22.0, "pmConfigFileRead() properly set metadata (F64)");
+        psBool tmpBool = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_BOOL_T");
+        ok(rc == true && tmpBool == true, "pmConfigFileRead() properly set metadata (bool)");
+        tmpBool = psMetadataLookupS32(&rc, config, "ARBITRARY_STRING_BOOL_F");
+        ok(rc == true && tmpBool == false, "pmConfigFileRead() properly set metadata (bool)");
+        psFree(config);
+        pmConfigDone();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigValidateCameraFormat()
+    // Test with NULL valid pointer
+    // XXX: This is commented out because of a seg-fault
+    if (0) {
+        psMemId id = psMemGetId();
+        psMetadata *cameraFormat = psMetadataAlloc();
+        psMetadata *header = psMetadataAlloc();
+        bool rc = pmConfigValidateCameraFormat(NULL, cameraFormat, header);
+        ok(rc == false, "pmConfigValidateCameraFormat() returned FALSE with NULL valid pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigValidateCameraFormat() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(cameraFormat);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with NULL cameraFormat pointer
+    {
+        psMemId id = psMemGetId();
+        bool valid;
+        psMetadata *cameraFormat = psMetadataAlloc();
+        psMetadata *header = psMetadataAlloc();
+        bool rc = pmConfigValidateCameraFormat(&valid, NULL, header);
+        ok(rc == false, "pmConfigValidateCameraFormat() returned FALSE with NULL valid pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigValidateCameraFormat() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(cameraFormat);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with NULL header pointer
+    {
+        psMemId id = psMemGetId();
+        bool valid;
+        psMetadata *cameraFormat = psMetadataAlloc();
+        psMetadata *header = psMetadataAlloc();
+        bool rc = pmConfigValidateCameraFormat(&valid, cameraFormat, NULL);
+        ok(rc == false, "pmConfigValidateCameraFormat() returned FALSE with NULL valid pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigValidateCameraFormat() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(cameraFormat);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Test with acceptable input
+    // psTraceSetLevel("psModules.config", 10);
+    {
+        psMemId id = psMemGetId();
+        bool valid;
+        psMetadata *header = psMetadataAlloc();
+        psMetadata *camera = psMetadataAlloc();
+        // Test with unitialized camera metadata
+        bool rc = pmConfigValidateCameraFormat(&valid, camera, header);
+        ok(rc == false && valid == false, "pmConfigValidateCameraFormat() returned FALSE with uninitialized psMetadata");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_UNKNOWN == tmpErr->code,
+          "pmConfigValidateCameraFormat() created the PS_ERR_UNKNOWN error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(camera);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Test with acceptable input
+    // psTraceSetLevel("psModules.config", 10);
+    // Add a test that sets the metadata value incorrectly
+    {
+        psMemId id = psMemGetId();
+        bool valid;
+        psMetadata *header = psMetadataAlloc();
+        psMetadata *camera = NULL;
+        bool rc = pmConfigFileRead(&camera, "data/camera0/format0.config", "Camera 0 Config File");
+        ok(rc == true, "pmConfigFileRead() read data/camera0/format0.config");
+        psMetadataAddStr(header, PS_LIST_TAIL, "F0_KEY0", 0, "", "string20");
+        rc = pmConfigValidateCameraFormat(&valid, camera, header);
+        ok(rc == true, "pmConfigValidateCameraFormat() returned FALSE with acceptable input, wrong values");
+        ok(valid == false, "pmConfigValidateCameraFormat() rejected header correctly");
+        psMetadataAddStr(header, PS_LIST_TAIL, "F0_KEY1", 0, "", "string21");
+        rc = pmConfigValidateCameraFormat(&valid, camera, header);
+        ok(rc == true, "pmConfigValidateCameraFormat() returned FALSE with acceptable input, wrong values");
+        ok(valid == false, "pmConfigValidateCameraFormat() rejected header correctly");
+        psMetadataAddS32(header, PS_LIST_TAIL, "F0_KEY2", 0, "", 20);
+        rc = pmConfigValidateCameraFormat(&valid, camera, header);
+        ok(rc == true, "pmConfigValidateCameraFormat() returned TRUE with acceptable input, correct values");
+        ok(valid == true, "pmConfigValidateCameraFormat() accepted header correctly");
+        psFree(camera);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigCameraFormatFromHeader()
+    //
+    // Given a FITS header, check it against all known cameras (unless we
+    // already know which camera, from pmConfigRead) and all known formats
+    // for those cameras in order to identify which is appropriate.
+    //
+    // psMetadata *pmConfigCameraFormatFromHeader(
+    //    pmConfig *config,
+    //    const psMetadata *header,
+    //    bool readRecipes)
+    // Test with NULL config pointer
+    {
+        psMemId id = psMemGetId();
+        pmConfig *config = pmConfigAlloc();
+        psMetadata *header = psMetadataAlloc();
+        bool readRecipes = false;
+        psMetadata *camera = pmConfigCameraFormatFromHeader(NULL, header, readRecipes);
+        ok(camera == NULL, "pmConfigCameraFormatFromHeader() returned NULL with NULL config pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigCameraFormatFromHeader() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(config);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with NULL header pointer
+    {
+        psMemId id = psMemGetId();
+        pmConfig *config = pmConfigAlloc();
+        psMetadata *header = psMetadataAlloc();
+        bool readRecipes = false;
+        psMetadata *camera = pmConfigCameraFormatFromHeader(config, NULL, readRecipes);
+        ok(camera == NULL, "pmConfigCameraFormatFromHeader() returned NULL with NULL header pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigCameraFormatFromHeader() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(config);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Test with acceptable inputs
+    if (1) {
+        psMemId id = psMemGetId();
+        pmConfig *config = pmConfigAlloc();
+        psMetadata *header = psMetadataAlloc();
+        bool readRecipes = false;
+        // Load a sample config file
+        bool rc = pmConfigFileRead(&config->site, "data/SampleIPPConfig", "DESCRIPTION");
+        ok(rc == true && !config, "pmConfigFileRead() was successful");
+        // Set metadata tags for a simulated FITS header
+        psMetadataAddStr(header, PS_LIST_TAIL, "F0_KEY0", 0, "", "string20");
+        psMetadataAddStr(header, PS_LIST_TAIL, "F0_KEY1", 0, "", "string21");
+        psMetadataAddS32(header, PS_LIST_TAIL, "F0_KEY2", 0, "", 20);
+        psMetadata *camera = pmConfigCameraFormatFromHeader(config, header, readRecipes);
+
+        ok(camera != NULL,
+          "pmConfigCameraFormatFromHeader() returned non-NULL with acceptable inputs");
+        ok(camera != NULL && psMemCheckMetadata(camera),
+          "pmConfigCameraFormatFromHeader() returned non-NULL and correct type with acceptable inputs");
+        psFree(config);
+        psFree(header);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigCameraByName()
+    //    psMetadata *pmConfigCameraByName(pmConfig *config, const char *cameraName)
+    // Test with NULL config pointer
+    {
+        psMemId id = psMemGetId();
+        pmConfig *config = pmConfigAlloc();
+        psString cameraName = "CameraName";
+        psMetadata *camera = pmConfigCameraByName(NULL, cameraName);
+        ok(camera == NULL, "pmConfigCameraByName() returned NULL with NULL config pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigCameraByName() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with NULL camera Name
+    {
+        psMemId id = psMemGetId();
+        pmConfig *config = pmConfigAlloc();
+        // psString cameraName = "CameraName";
+        psMetadata *camera = pmConfigCameraByName(config, NULL);
+        ok(camera == NULL, "pmConfigCameraByName() returned NULL with NULL camera name");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_VALUE == tmpErr->code,
+          "pmConfigCameraByName() created the PS_ERR_BAD_PARAMETER_VALUE error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    psTraceSetLevel("err", 10);
+    // Test with acceptable data
+    if (1) {
+        psMemId id = psMemGetId();
+        pmConfigSet("data");
+        pmConfig *config = pmConfigAlloc();
+        bool rc = pmConfigFileRead(&config->site, "data/SampleIPPConfig", "DESCRIPTION");
+        ok(rc == true, "pmConfigFileRead() was successful");
+        psString cameraName = "CAMERA0";
+        psMetadata *camera = pmConfigCameraByName(config, cameraName);
+        ok(camera != NULL && psMemCheckMetadata(camera),
+          "pmConfigCameraByName() returned non-NULL with acceptable");
+        char *tmpStr = psMetadataLookupStr(&rc, camera, "ID");
+        ok(tmpStr != NULL && !strcmp(tmpStr, "CAMERA0"), "pmConfigCameraByName() returned non-NULL(CAMERA0)");
+        pmConfigDone();
+        psFree(config);
+        psFree(tmpStr);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigDB()
+    // psDB *pmConfigDB(pmConfig *config)
+    // Test with NULL config pointer
+    {
+        psMemId id = psMemGetId();
+        pmConfig *config = pmConfigAlloc();
+        psDB *db = pmConfigDB(NULL);
+        ok(db == NULL, "pmConfigDB() returned NULL with NULL config pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigDB()() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with NULL config->site pointer
+    {
+        psMemId id = psMemGetId();
+        pmConfig *config = pmConfigAlloc();
+        config->site = NULL;
+        psDB *db = pmConfigDB(config);
+        ok(db == NULL, "pmConfigDB() returned NULL with NULL config->site pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigDB()() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Test with acceptable data
+    // XXX: Not tested.
+    if (1) {
+        psMemId id = psMemGetId();
+        pmConfig *config = pmConfigAlloc();
+        config->site = psMetadataAlloc();
+        bool rc = pmConfigFileRead(&config->site, "data/SampleIPPConfig", "DESCRIPTION");
+        ok(rc == true, "pmConfigFileRead() was successful");
+        skip_start(rc == false, 1, "Skipping pmConfigDB() tests because we could not read config file");
+        psDB *db = pmConfigDB(config);
+        ok(db != NULL, "pmConfigDB() returned non NULL with acceptable data");
+        psFree(db);
+        skip_end();
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigConformHeader()
+    // Test with NULL header pointer
+    {
+        psMemId id = psMemGetId();
+        psMetadata *header = psMetadataAlloc();
+        psMetadata *format = psMetadataAlloc();
+        bool rc = pmConfigConformHeader(NULL, (const psMetadata *) format);
+        ok(rc == false, "pmConfigConformHeader() returned FALSE with NULL header pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigConformHeader() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(header);
+        psFree(format);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with NULL format pointer
+    {
+        psMemId id = psMemGetId();
+        psMetadata *header = psMetadataAlloc();
+        psMetadata *format = psMetadataAlloc();
+        bool rc = pmConfigConformHeader(header, NULL);
+        ok(rc == false, "pmConfigConformHeader() returned FALSE with NULL header pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigConformHeader() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(header);
+        psFree(format);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Test with acceptable data
+    {
+        psMemId id = psMemGetId();
+        bool valid;
+        psMetadata *header = psMetadataAlloc();
+        psMetadata *format = psMetadataAlloc();
+        bool rc = pmConfigFileRead(&format, "data/camera0/format0.config", "Camera 0 Config File");
+        ok(rc == true, "pmConfigFileRead() read camera format correctly");
+
+        // First ensure that the header is not accepted
+        rc = pmConfigValidateCameraFormat(&valid, format, header);
+        ok(rc == true && valid == false, "pmConfigValidateCameraFormat() rejected header with uninitialized psMetadata");
+
+        // Now call pmConfigConformHeader() and ensure that the header is accepted
+        rc = pmConfigConformHeader(header, format);
+        ok(rc == true, "pmConfigConformHeader() returned TRUE");
+        rc = pmConfigValidateCameraFormat(&valid, format, header);
+        ok(rc == true, "pmConfigValidateCameraFormat() returned TRUE");
+        ok(valid == true, "pmConfigValidateCameraFormat() validated camera format");
+        psFree(header);
+        psFree(format);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigFileSets()
+    // test with NULL argc pointer
+    {
+        psMemId id = psMemGetId();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        char *filename = "FILENAME";
+        char *list = "LIST";
+        psArray *array = pmConfigFileSets(NULL, str, filename, list);
+        ok(array == NULL, "pmConfigFileSets() returned NULL with NULL argc pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigFileSets() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test with negative argc pointer
+    {
+        psMemId id = psMemGetId();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        int myArgc = -1;
+        char *filename = "FILENAME";
+        char *list = "LIST";
+        psArray *array = pmConfigFileSets(&myArgc, str, filename, list);
+        ok(array == NULL, "pmConfigFileSets() returned NULL with negative argc pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_VALUE == tmpErr->code,
+          "pmConfigFileSets() created the PS_ERR_BAD_PARAMETER_VALUE error");
+        psFree(tmpErr);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test with NULL argv pointer
+    {
+        psMemId id = psMemGetId();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        int myArgc = 3;
+        char *filename = "FILENAME";
+        char *list = "LIST";
+        psArray *array = pmConfigFileSets(&myArgc, NULL, filename, list);
+        ok(array == NULL, "pmConfigFileSets() returned NULL with NULL argv pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigFileSets() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test with NULL filename pointer
+    {
+        psMemId id = psMemGetId();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        int myArgc = 3;
+        char *list = "LIST";
+        psArray *array = pmConfigFileSets(&myArgc, str, NULL, list);
+        ok(array == NULL, "pmConfigFileSets() returned NULL with NULL filename pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_VALUE == tmpErr->code,
+          "pmConfigFileSets() created the PS_ERR_BAD_PARAMETER_VALUE error");
+        psFree(tmpErr);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test with NULL list pointer
+    {
+        psMemId id = psMemGetId();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        int myArgc = 3;
+        char *filename = "FILENAME";
+        psArray *array = pmConfigFileSets(&myArgc, str, filename, NULL);
+        ok(array == NULL, "pmConfigFileSets() returned NULL with NULL list pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_VALUE == tmpErr->code,
+          "pmConfigFileSets() created the PS_ERR_BAD_PARAMETER_VALUE error");
+        psFree(tmpErr);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test with acceptable data
+    if (0) {
+        psMemId id = psMemGetId();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        int myArgc = 3;
+        char *filename = "FILENAME";
+        char *list = "LIST";
+        psArray *array = pmConfigFileSets(&myArgc, str, filename, list);
+        ok(array != NULL && psMemCheckArray(array),
+          "pmConfigFileSets() returned non-NULL with acceptable input data");
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigFileSetsMD()
+    // bool pmConfigFileSetsMD(psMetadata *metadata, int *argc, char **argv, const char *name,
+    //                         const char *file, const char *list)
+    // XX: Should we test/check for bad argv/argc params?
+    // test with NULL metadata pointer
+    {
+        psMemId id = psMemGetId();
+        psMetadata *metadata = psMetadataAlloc();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        int myArgc = 3;
+        char *name = "NAME";
+        char *file = "FILENAME";
+        char *list = "LIST";
+        bool rc = pmConfigFileSetsMD(NULL, &myArgc, str, name, file, list);
+        ok(rc == false, "pmConfigFileSetsMD() returned FALSE with NULL metadata pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigFileSetsMD() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psFree(metadata);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test with NULL name pointer
+    {
+        psMemId id = psMemGetId();
+        psMetadata *metadata = psMetadataAlloc();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        int myArgc = 3;
+        char *file = "FILENAME";
+        char *list = "LIST";
+        bool rc = pmConfigFileSetsMD(metadata, &myArgc, str, NULL, file, list);
+        ok(rc == false, "pmConfigFileSetsMD() returned FALSE with NULL name pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_VALUE == tmpErr->code,
+          "pmConfigFileSetsMD() created the PS_ERR_BAD_PARAMETER_VALUE error");
+        psFree(tmpErr);
+        psFree(metadata);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test with NULL file pointer
+    {
+        psMemId id = psMemGetId();
+        psMetadata *metadata = psMetadataAlloc();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        int myArgc = 3;
+        char *name = "NAME";
+        char *list = "LIST";
+        bool rc = pmConfigFileSetsMD(metadata, &myArgc, str, name, NULL, list);
+        ok(rc == false, "pmConfigFileSetsMD() returned FALSE with NULL file pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_VALUE == tmpErr->code,
+          "pmConfigFileSetsMD() created the PS_ERR_BAD_PARAMETER_VALUE error");
+        psFree(tmpErr);
+        psFree(metadata);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test with NULL list pointer
+    {
+        psMemId id = psMemGetId();
+        psMetadata *metadata = psMetadataAlloc();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        int myArgc = 3;
+        char *name = "NAME";
+        char *file = "FILENAME";
+        bool rc = pmConfigFileSetsMD(metadata, &myArgc, str, name, file, NULL);
+        ok(rc == false, "pmConfigFileSetsMD() returned FALSE with NULL list pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_VALUE == tmpErr->code,
+          "pmConfigFileSetsMD() created the PS_ERR_BAD_PARAMETER_VALUE error");
+        psFree(tmpErr);
+        psFree(metadata);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test with acceptable input data
+    if (0) {
+        psMemId id = psMemGetId();
+        psMetadata *metadata = psMetadataAlloc();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        int myArgc = 3;
+        char *name = "NAME";
+        char *file = "FILENAME";
+        char *list = "LIST";
+        bool rc = pmConfigFileSetsMD(metadata, &myArgc, str, name, file, list);
+        ok(rc == true, "pmConfigFileSetsMD() returned TRUE with acceptable input data");
+        psFree(metadata);
+        psErrorClear();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigConvertFilename()
+    // convert the supplied name, create a new output psString
+    // psString pmConfigConvertFilename(const char *filename, const pmConfig *config,
+    //                                  bool create)
+    // test with NULL filename pointer
+    {
+        psMemId id = psMemGetId();
+        pmConfig *config = pmConfigAlloc();
+        bool create = false;
+        bool rc = pmConfigConvertFilename(NULL, (const pmConfig *) config, create);
+        ok(rc == false, "pmConfigConvertFilename() returned FALSE with NULL filename pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_VALUE == tmpErr->code,
+          "pmConfigConvertFilename() created the PS_ERR_BAD_PARAMETER_VALUE error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test with NULL config pointer
+    {
+        psMemId id = psMemGetId();
+        char *name = "NAME";
+        pmConfig *config = pmConfigAlloc();
+        bool create = false;
+        bool rc = pmConfigConvertFilename(name, NULL, create);
+        ok(rc == false, "pmConfigConvertFilename() returned FALSE with NULL config pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigConvertFilename() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+    // test with acceptable input data
+    // First call with create==FALSE, and a filename that does not exist.
+    // Should return NULL.  Then set create==TRUE, and call; should return
+    // "JUNK".  Then call again with create==FALSE; should return "JUNK"
+    if (1) {
+        psMemId id = psMemGetId();
+        char *filename = "file:///////JUNK";
+        pmConfig *config = pmConfigAlloc();
+        bool rc = pmConfigFileRead(&config->site, "data/SampleIPPConfig", "DESCRIPTION");
+        ok(rc == true, "pmConfigFileRead() was successful");
+        bool create = false;
+        psString tmpStr = pmConfigConvertFilename(filename, (const pmConfig *) config, create);
+        ok(NULL == tmpStr, "pmConfigConvertFilename() returned NULL with create==FALSE");
+        create = true;
+        tmpStr = pmConfigConvertFilename(filename, (const pmConfig *) config, create);
+        ok(tmpStr != NULL, "pmConfigConvertFilename() returned non-NULL with create==TRUE");
+        ok(!strcmp(tmpStr, "/JUNK"), "pmConfigConvertFilename() returned correct filename (%s)", tmpStr);
+        psFree(tmpStr);
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+    // --------------------------------------------------------------------
+    // --------------------------------------------------------------------
+    // Test pmConfigRead()
+    // Test with NULL argc pointer
+    {
+        psMemId id = psMemGetId();
+        psString str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        pmConfig *myConfig = pmConfigRead(NULL, str, "RecipeName");
+        ok(myConfig == NULL, "pmConfigRead() returned NULL with NULL argc pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigValidateCameraFormat() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(myConfig);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with NULL argv pointer
+    {
+        psMemId id = psMemGetId();
+        pmConfig *myConfig = pmConfigRead(&argc, NULL, "RecipeName");
+        ok(myConfig == NULL, "pmConfigRead() returned NULL with NULL argv pointer");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigValidateCameraFormat() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(myConfig);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test with NULL recipe string (should not cause error)
+    // XXX: Not working, debug
+    if (0) {
+        psMemId id = psMemGetId();
+        char *str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        int numArgs = 3;
+        pmConfig *myConfig = pmConfigRead(&numArgs, str, NULL);
+        ok(myConfig != NULL, "pmConfigRead() returned non-NULL with NULL recipe");
+        psErr *tmpErr = psErrorLast();
+        ok(PS_ERR_BAD_PARAMETER_NULL == tmpErr->code,
+          "pmConfigValidateCameraFormat() created the PS_ERR_BAD_PARAMETER_NULL error");
+        psFree(tmpErr);
+        psErrorClear();
+        psFree(myConfig);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmConfigRead() with acceptable data.
+    if (1) {
+        psMemId id = psMemGetId();
+        bool testStatus = true;
+        psMetadata *site = psMetadataAlloc();
+        psMetadata *camera = psMetadataAlloc();
+        psMetadata *recipe = psMetadataAlloc();
+        psString str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        psS32 argc = 3;
+        printf("----------------------------------------------------------------\n");
+        printf("Calling pmConfigRead() with acceptable arguments.\n");
+        bool rc;
+        pmConfig *myConfig = pmConfigRead(&argc, str, "RecipeName");
+        ok(myConfig, "pmConfigRead() returned non-NULL");
+        if (myConfig == NULL) {
+            printf("TEST ERROR: pmConfigRead() returned NULL\n");
+            testStatus = false;
+        } else {
+            //
+            // Ensure that the various trace and logging values are set properly
+            //
+            if (1 != psTraceGetLevel("dummyTraceFunc01")) {
+                printf("TEST ERROR: failed to properly set tracelevel for dummyTraceFunc01\n");
+                printf("    tracelevel was %d, should be 1.\n", psTraceGetLevel("dummyTraceFunc01"));
+                testStatus = false;
+            }
+
+            if (2 != psTraceGetLevel("dummyTraceFunc02")) {
+                printf("TEST ERROR: failed to properly set tracelevel for dummyTraceFunc02\n");
+                printf("    tracelevel was %d, should be 2.\n", psTraceGetLevel("dummyTraceFunc02"));
+                testStatus = false;
+            }
+
+            if (1 != psLogGetDestination()) {
+                printf("TEST ERROR: failed to properly set log destination.\n");
+                printf("    it was %d, should be 1\n", psLogGetDestination());
+                testStatus = false;
+            }
+
+            if (3 != psLogGetLevel()) {
+                printf("TEST ERROR: failed to properly set log level.\n");
+                printf("    it was %d, should be 3\n", psLogGetLevel());
+                testStatus = false;
+            }
+
+            //
+            // Test the several arbitrary config file strings.
+            //
+            psS32 tmpInt = psMetadataLookupS32(&rc, site, "ARBITRARY_STRING_S32");
+            if ((rc == false) || (tmpInt != 20)) {
+                printf("TEST ERROR: failed to properly set metadata integer ARBITRARY_STRING_S32.\n");
+                testStatus = false;
+            }
+
+            psF32 tmpFloat = psMetadataLookupS32(&rc, site, "ARBITRARY_STRING_F32");
+            if ((rc == false) || (tmpFloat != 21.0)) {
+                printf("TEST ERROR: failed to properly set metadata float ARBITRARY_STRING_F32.\n");
+                testStatus = false;
+            }
+
+            psF64 tmpDub = psMetadataLookupS32(&rc, site, "ARBITRARY_STRING_F64");
+            if ((rc == false) || (tmpDub != 22.0)) {
+                printf("TEST ERROR: failed to properly set metadata double ARBITRARY_STRING_F64.\n");
+                testStatus = false;
+            }
+
+            psBool tmpBool;
+            tmpBool = psMetadataLookupS32(&rc, site, "ARBITRARY_STRING_BOOL_T");
+            if ((rc == false) || (tmpBool != true)) {
+                printf("TEST ERROR: failed to properly set metadata double ARBITRARY_STRING_BOOL_T.\n");
+                testStatus = false;
+            }
+            tmpBool = psMetadataLookupS32(&rc, site, "ARBITRARY_STRING_BOOL_F");
+            if ((rc == false) || (tmpBool != false)) {
+                printf("TEST ERROR: failed to properly set metadata double ARBITRARY_STRING_BOOL_F.\n");
+                testStatus = false;
+            }
+
+            //
+            // Test the database camera metadata keywords.
+            //
+            psMetadata *tmpMeta = psMetadataLookupMetadata(&rc, site, "CAMERAS");
+            if (rc == false) {
+                printf("TEST ERROR: failed to properly set metadata metadata CAMERAS.\n");
+                testStatus = false;
+            } else {
+                psString tmpStr;
+                tmpStr = psMetadataLookupStr(&rc, tmpMeta, "MEGACAM_RAW");
+                if ((rc == false) || (0 != strcmp(tmpStr, "megacam_raw.config"))) {
+                    printf("TEST ERROR: failed to properly set metadata metadata CAMERAS->MEGACAM_RAW.\n");
+                    testStatus = false;
+                }
+
+                tmpStr = psMetadataLookupStr(&rc, tmpMeta, "MEGACAM_SPLICE");
+                if ((rc == false) || (0 != strcmp(tmpStr, "megacam_splice.config"))) {
+                    printf("TEST ERROR: failed to properly set metadata metadata CAMERAS->MEGACAM_SPLICE.\n");
+                    testStatus = false;
+                }
+
+                tmpStr = psMetadataLookupStr(&rc, tmpMeta, "GPC1_RAW");
+                if ((rc == false) || (0 != strcmp(tmpStr, "gpc1_raw.config"))) {
+                    printf("TEST ERROR: failed to properly set metadata metadata CAMERAS->GPC1_RAW.\n");
+                    testStatus = false;
+                }
+
+                tmpStr = psMetadataLookupStr(&rc, tmpMeta, "LRIS_BLUE");
+                if ((rc == false) || (0 != strcmp(tmpStr, "lris_blue.config"))) {
+                    printf("TEST ERROR: failed to properly set metadata metadata CAMERAS->LRIS_BLUE.\n");
+                    testStatus = false;
+                }
+
+                tmpStr = psMetadataLookupStr(&rc, tmpMeta, "LRIS_RED");
+                if ((rc == false) || (0 != strcmp(tmpStr, "lris_red.config"))) {
+                    printf("TEST ERROR: failed to properly set metadata metadata CAMERAS->LRIS_RED.\n");
+                    testStatus = false;
+                }
+            }
+
+            //
+            // Test the database metadata keywords.  This is somewhat redundant since
+            // we already test metadat strings.
+            //
+            psString tmpStr;
+            tmpStr = psMetadataLookupStr(&rc, site, "DBSERVER");
+            if ((rc == false) || (0 != strcmp(tmpStr, "ippdb.ifa.hawaii.edu"))) {
+                printf("TEST ERROR: failed to properly set metadata string DBSERVER.\n");
+                testStatus = false;
+            }
+
+            tmpStr = psMetadataLookupStr(&rc, site, "DBUSER");
+            if ((rc == false) || (0 != strcmp(tmpStr, "ipp"))) {
+                printf("TEST ERROR: failed to properly set metadata string DBUSER.\n");
+                testStatus = false;
+            }
+
+            tmpStr = psMetadataLookupStr(&rc, site, "DBPASSWORD");
+            if ((rc == false) || (0 != strcmp(tmpStr, "password"))) {
+                printf("TEST ERROR: failed to properly set metadata string DBPASSWORD.\n");
+                testStatus = false;
+            }
+        }
+        psFree(site);
+        psFree(camera);
+        psFree(recipe);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+}
+
Index: /branches/eam_branch_20071023/psModules/test/config/tap_pmConfigCommand.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/tap_pmConfigCommand.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/tap_pmConfigCommand.c	(revision 15358)
@@ -0,0 +1,143 @@
+/** @file tst_pmConfigCommand.c
+ *
+ *  @brief Contains the tests for pmConfigCommand.c:
+ *
+ * This code will test the pmConfigCommand() routine.
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-18 18:58:52 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+#define ERR_TRACE_LEVEL         0
+#define VERBOSE			0
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel(".", 0);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(14);
+
+
+    // --------------------------------------------------------------------
+    // pmConfigDatabaseCommand() tests
+    // Test pmConfigDatabaseCommand() with NULL command input param
+    // XXX: I think the memory leak is in pmConfigRead()
+    {
+        psMemId id = psMemGetId();
+        psString str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        psS32 argc = 3;
+        pmConfig *config = pmConfigRead(&argc, str, "RecipeName");
+        ok(config != NULL, "pmConfigRead() successful");
+        ok(!pmConfigDatabaseCommand(NULL, config), "pmConfigDatabaseCommand() returned NULL with NULL command input param");
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmConfigDatabaseCommand() with NULL pmConfig input param
+    // XXX: I think the memory leak is in pmConfigRead()
+    {
+        psMemId id = psMemGetId();
+        psString testCmd = psStringCopy("my command");
+        ok(!pmConfigDatabaseCommand(&testCmd, NULL), "pmConfigDatabaseCommand() returned NULL with NULL command input param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmConfigDatabaseCommand() with command arg generated by other than psStringDup()
+    // XXX: This seg-faults because pmConfigDatabaseCommand(), or whichever function,
+    // does not verify that the command was allocated as a psString.
+    if (0) {
+        psMemId id = psMemGetId();
+        psString str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        psS32 argc = 3;
+        pmConfig *config = pmConfigRead(&argc, str, "RecipeName");
+        ok(config != NULL, "pmConfigRead() successful");
+        char *testCmd = "command";
+        ok(!pmConfigDatabaseCommand(&testCmd, config), "pmConfigDatabaseCommand() returned NULL with NULL command input param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmConfigDatabaseCommand() with acceptable input params
+    // XXX: I think the memory leak is in pmConfigRead()
+    {
+        psMemId id = psMemGetId();
+        psString str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        psS32 argc = 3;
+        pmConfig *config = pmConfigRead(&argc, str, "RecipeName");
+        ok(config != NULL, "pmConfigRead() successful");
+        psString testCmd = psStringCopy("my command");
+        psString verifyCmd = psStringCopy(testCmd);
+        // Generate the verify test string
+        {
+            bool mdok;
+            psString dbserver = psMetadataLookupStr(&mdok, config->site, "DBSERVER");
+            psString dbname = psMetadataLookupStr(&mdok, config->site, "DBNAME");
+            psString dbuser = psMetadataLookupStr(&mdok, config->site, "DBUSER");
+            psString dbpassword = psMetadataLookupStr(&mdok, config->site, "DBPASSWORD");
+            psStringAppend(&verifyCmd, " -dbserver %s -dbname %s -dbuser %s -dbpassword %s",
+                           dbserver, dbname, dbuser, dbpassword);
+	}
+
+        ok(pmConfigDatabaseCommand(&testCmd, config), "pmConfigDatabaseCommand() returned non-NULL with acceptable input params");
+        ok(!strcmp(testCmd, verifyCmd), "pmConfigDatabaseCommand() generated the correct command string");
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // --------------------------------------------------------------------
+    // pmConfigTraceCommand() tests
+    // Test pmConfigTraceCommand() with NULL command input param
+    {
+        psMemId id = psMemGetId();
+        ok(!pmConfigTraceCommand(NULL), "pmConfigTraceCommand() returned NULL with NULL command input param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmConfigTraceCommand() with acceptable input command input param
+    {
+        psMemId id = psMemGetId();
+        psTraceSetLevel("dummyTraceLevel", 0);
+        psString testCmd = psStringCopy("my command");
+        psString verifyCmd = psStringCopy(testCmd);
+        // Generate the verify test string
+        {
+            psMetadata *levels = psTraceLevels();
+            psMetadataIterator *iter = psMetadataIteratorAlloc(levels, PS_LIST_HEAD, NULL);
+            psMetadataItem *item;
+            while ((item = psMetadataGetAndIncrement(iter))) {
+                assert(item->type == PS_TYPE_S32);
+                psStringAppend(&verifyCmd, " -trace %s %d", item->name, item->data.S32);
+            }
+            psFree(iter);
+            psFree(levels);
+	}
+        ok(pmConfigTraceCommand(&testCmd), "pmConfigTraceCommand() returned non-NULL with acceptable input param");
+        ok(!strcmp(testCmd, verifyCmd), "pmConfigTraceCommand() generated the correct command string");
+        psFree(testCmd);
+        psFree(verifyCmd);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+    
Index: /branches/eam_branch_20071023/psModules/test/config/tap_pmConfigMask.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/tap_pmConfigMask.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/tap_pmConfigMask.c	(revision 15358)
@@ -0,0 +1,79 @@
+/** @file tst_pmConfigMask.c
+ *
+ *  @brief Contains the tests for pmConfigMask.c:
+ *
+ * This code will test the pmConfigMask() routine.
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-18 18:58:56 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+#define ERR_TRACE_LEVEL         0
+#define VERBOSE			0
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel(".", 0);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(9);
+
+
+    // --------------------------------------------------------------------
+    // pmConfigMask() tests
+    // Test pmConfigMask() with NULL masks input param
+    // XXX: I think the memory leak is in pmConfigRead()
+    {
+        psMemId id = psMemGetId();
+        psString str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        psS32 argc = 3;
+        pmConfig *config = pmConfigRead(&argc, str, "RecipeName");
+        ok(config != NULL, "pmConfigRead() successful");
+        ok(!pmConfigMask(NULL, config), "pmConfigMask() returned NULL with NULL masks input param");
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmConfigMask() with NULL pmConfig input param
+    // XXX: This fails on current CVS code because there is no assert in pmConfigMask() for a null pmConfig param
+    {
+        psMemId id = psMemGetId();
+        char *masks = "Mask0 Mask1 Mask2";
+        ok(!pmConfigMask(masks, NULL), "pmConfigMask() returned NULL with NULL pmConfig input param");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmConfigMask() with acceptable input params
+    // XXX: I think the memory leak is in pmConfigRead()
+    {
+        psMemId id = psMemGetId();
+        // See file TESTDIR/config/data/recipes_masks.config (
+        char *masks = "DETECTOR RANGE";
+        psMaskType correctMask = 0x02 | 0x04;
+        psString str[3];
+        str[0] = "ARGS:";
+        str[1] = "-site";
+        str[2] = "data/SampleIPPConfig";
+        psS32 argc = 3;
+        pmConfig *config = pmConfigRead(&argc, str, "RecipeName");
+        ok(config != NULL, "pmConfigRead() successful");
+        psMaskType mask = pmConfigMask(masks, config);
+        ok(mask, "pmConfigMask non-zero with acceptable input params");
+        ok(mask == correctMask, "pmConfigMask() generated the correct output mask (%x).  Should be (%x)", mask, correctMask);
+        psFree(config);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branch_20071023/psModules/test/config/tap_pmErrorCodes.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/tap_pmErrorCodes.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/tap_pmErrorCodes.c	(revision 15358)
@@ -0,0 +1,98 @@
+/** @file tst_pmErrorCodes.c
+ *
+ *  @brief Contains the tests for pmErrorCodes.c:
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-18 18:58:58 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+#define ERR_TRACE_LEVEL         0
+#define VERBOSE			0
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel(".", 0);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(16);
+
+
+    // --------------------------------------------------------------------
+    // pmErrorRegister() tests
+    // Test pmErrorRegister() with currently coded error types
+    {
+        psMemId id = psMemGetId();
+        pmErrorRegister();
+        char *errStr;
+        errStr = (char *) psErrorCodeString(PM_ERR_BASE);
+        ok(!strcmp("First value we use; lower values belong to psLib", errStr),
+           "psErrorCodeString(PM_ERR_BASE)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_UNKNOWN);
+        ok(!strcmp("Unknown psModules error code", errStr),
+           "psErrorCodeString(PM_ERR_UNKNOWN)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_PHOTOM);
+        ok(!strcmp("Problem in photometry", errStr),
+           "psErrorCodeString(PM_ERR_PHOTOM)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_PSF);
+        ok(!strcmp("Problem in PSF", errStr),
+           "psErrorCodeString(PM_ERR_PSF)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_ASTROM);
+        ok(!strcmp("Problem in astrometry", errStr),
+           "psErrorCodeString(PM_ERR_ASTROM)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_CAMERA);
+        ok(!strcmp("Problem in camera", errStr),
+           "psErrorCodeString(PM_ERR_CAMERA)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_CONCEPTS);
+        ok(!strcmp("Problem in concepts", errStr),
+           "psErrorCodeString(PM_ERR_CONCEPTS)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_IMCOMBINE);
+        ok(!strcmp("Problem in imcombine", errStr),
+           "psErrorCodeString(PM_ERR_IMCOMBINE)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_OBJECTS);
+        ok(!strcmp("Problem in objects", errStr),
+           "psErrorCodeString(PM_ERR_OBJECTS)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_SKY);
+        ok(!strcmp("Problem in sky", errStr),
+           "psErrorCodeString(PM_ERR_SKY)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_ARGUMENTS);
+        ok(!strcmp("Incorrect arguments", errStr),
+           "psErrorCodeString(PM_ERR_ARGUMENTS)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_SYS);
+        ok(!strcmp("System error", errStr),
+           "psErrorCodeString(PM_ERR_SYS)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_CONFIG);
+        ok(!strcmp("Problem in configure files", errStr),
+           "psErrorCodeString(PM_ERR_CONFIG)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_PROG);
+        ok(!strcmp("Programming error", errStr),
+           "psErrorCodeString(PM_ERR_PROG)");
+
+        errStr = (char *) psErrorCodeString(PM_ERR_DATA);
+        ok(!strcmp("invalid data", errStr),
+           "psErrorCodeString(PM_ERR_DATA)");
+
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branch_20071023/psModules/test/config/tap_pmVersion.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/tap_pmVersion.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/tap_pmVersion.c	(revision 15358)
@@ -0,0 +1,58 @@
+/** @file tst_pmVersion.c
+ *
+ *  @brief Contains the tests for pmVersion.c:
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-09-18 18:59:00 $
+ *
+ *  XX: The output version string for psModulesVersionLong() is not verified.
+ *  Not sure how to do this since this psModulesVersionLong() produces the
+ *  time that the source code, not the test code, was compiled.
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+#include "config.h"
+#define ERR_TRACE_LEVEL         0
+#define VERBOSE			0
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel(".", 0);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(4);
+
+
+    // --------------------------------------------------------------------
+    // psModulesVersion() tests
+    // XX: The output string is not verified
+    {
+        psMemId id = psMemGetId();
+        psString tstStr = NULL;
+        psStringAppend(&tstStr, "%s-%s",PACKAGE_NAME,PACKAGE_VERSION);
+        psString str = psModulesVersion();
+        ok(!strcmp(str, tstStr), "psModulesVersion()");
+        psFree(str);
+        psFree(tstStr);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // --------------------------------------------------------------------
+    // psModulesVersionLong() tests
+    // XX: The output string is not verified
+    {
+        psMemId id = psMemGetId();
+        psString str = psModulesVersionLong();
+        ok(str, "psModulesVersionLong()");
+        psFree(str);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branch_20071023/psModules/test/config/tst_pmConfig.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/config/tst_pmConfig.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/config/tst_pmConfig.c	(revision 15358)
@@ -0,0 +1,277 @@
+/** @file tst_pmConfig.c
+ *
+ *  @brief Contains the tests for pmConfig.c:
+ *
+ * test00: This code will test the pmConfig() routine.
+ *
+ *  @author GLG, MHPCC
+ *
+ * XXX: Untested:
+ * pmConfigValidateCamera()
+ * pmConfigCameraFromHeader()
+ * pmConfigRecipeFromCamera()
+ * pmConfigDB()
+ *
+ * XXXX: Must determine what to do with NULL arguments, then test it.
+ *
+ *  @version $Revision: 1.8 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-10-13 21:15:45 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+#include "psTest.h"
+#include "pslib.h"
+#include "pmConfig.h"
+
+static int test00(void);
+testDescription tests[] = {
+                              {test00, 000, "pmConfig:", true, false},
+                              {NULL}
+                          };
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    //
+    // We include the function names here in psTraceSetLevel() commands for
+    // debugging convenience.  There is no guarantee that this list of functions
+    // is complete.
+    //
+    psTraceSetLevel(".", 0);
+    psTraceSetLevel("readConfig", 0);
+    psTraceSetLevel("pmConfigRead", 0);
+    psTraceSetLevel("pmConfigValidateCamera", 0);
+    psTraceSetLevel("pmConfigCameraFromHeader", 0);
+    psTraceSetLevel("pmConfigRecipeFromCamera", 0);
+    psTraceSetLevel("pmConfigDB", 0);
+    return !runTestSuite(stderr, "Test Point Driver", tests, argc, argv);
+}
+
+/******************************************************************************
+test00():
+XXX: untested:
+    TIME:
+    LOGFORMAT: tested implicitly via the stdout files.
+    Command line arguments for logging shall override config file defaults.
+ *****************************************************************************/
+int test00( void )
+{
+    psTraceSetLevel(".", 0);
+    bool testStatus = true;
+    psBool rc = 0;
+    psString str[3];
+    str[0] = "ARGS:";
+    str[1] = "-site";
+    str[2] = "SampleIPPConfig";
+
+    psMetadata *site = psMetadataAlloc();
+    psMetadata *camera = psMetadataAlloc();
+    psMetadata *recipe = psMetadataAlloc();
+    psS32 argc = 3;
+
+
+    psMetadata *nullMD = NULL;
+    if (0) {
+        printf("----------------------------------------------------------------\n");
+        printf("Calling pmConfigRead() with NULL site pointer.  Should generate ERROR, return false.\n");
+        rc = pmConfigRead(NULL, &camera, &recipe, &argc, str, "RecipeName");
+        if (rc == true) {
+            printf("TEST ERROR: pmConfigRead() returned true\n");
+            testStatus = false;
+        }
+
+        printf("----------------------------------------------------------------\n");
+        printf("Calling pmConfigRead() with NULL *site pointer.  Should generate ERROR, return false.\n");
+        rc = pmConfigRead(&nullMD, &camera, &recipe, &argc, str, "RecipeName");
+        if (rc == true) {
+            printf("TEST ERROR: pmConfigRead() returned true\n");
+            testStatus = false;
+        }
+
+        printf("----------------------------------------------------------------\n");
+        printf("Calling pmConfigRead() with NULL camera pointer.  Should generate ERROR, return false.\n");
+        rc = pmConfigRead(&site, NULL, &recipe, &argc, str, "RecipeName");
+        if (rc == true) {
+            printf("TEST ERROR: pmConfigRead() returned true\n");
+            testStatus = false;
+        }
+
+        printf("----------------------------------------------------------------\n");
+        printf("Calling pmConfigRead() with NULL *camera pointer.  Should generate ERROR, return false.\n");
+        rc = pmConfigRead(&site, &nullMD, &recipe, &argc, str, "RecipeName");
+        if (rc == true) {
+            printf("TEST ERROR: pmConfigRead() returned true\n");
+            testStatus = false;
+        }
+
+        printf("----------------------------------------------------------------\n");
+        printf("Calling pmConfigRead() with NULL recipe pointer.  Should generate ERROR, return false.\n");
+        rc = pmConfigRead(&site, &camera, NULL, &argc, str, "RecipeName");
+        if (rc == true) {
+            printf("TEST ERROR: pmConfigRead() returned true\n");
+            testStatus = false;
+        }
+
+        printf("----------------------------------------------------------------\n");
+        printf("Calling pmConfigRead() with NULL *recipe pointer.  Should generate ERROR, return false.\n");
+        rc = pmConfigRead(&site, &camera, &nullMD, &argc, str, "RecipeName");
+        if (rc == true) {
+            printf("TEST ERROR: pmConfigRead() returned true\n");
+            testStatus = false;
+        }
+
+        printf("----------------------------------------------------------------\n");
+        printf("Calling pmConfigRead() with NULL argv pointer.  Should generate ERROR, return false.\n");
+        rc = pmConfigRead(&site, &camera, &recipe, &argc, NULL, "RecipeName");
+        if (rc == true) {
+            printf("TEST ERROR: pmConfigRead() returned true\n");
+            testStatus = false;
+        }
+
+        printf("----------------------------------------------------------------\n");
+        printf("Calling pmConfigRead() with negative argc.  Should generate ERROR, return false.\n");
+        psS32 tmpArgc = -1;
+        rc = pmConfigRead(&site, &camera, &recipe, &tmpArgc, str, "RecipeName");
+        if (rc == true) {
+            printf("TEST ERROR: pmConfigRead() returned true\n");
+            testStatus = false;
+        }
+    }
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmConfigRead() with acceptable arguments.\n");
+
+    rc = pmConfigRead(&site, &camera, &recipe, &argc, str, "RecipeName");
+    if (rc == false) {
+        printf("TEST ERROR: pmConfigRead() returned False\n");
+        testStatus = false;
+    } else {
+        //
+        // Ensure that the various trace and logging values are set properly
+        //
+        if (1 != psTraceGetLevel("dummyTraceFunc01")) {
+            printf("TEST ERROR: failed to properly set tracelevel for dummyTraceFunc01\n");
+            printf("    tracelevel was %d, should be 1.\n", psTraceGetLevel("dummyTraceFunc01"));
+            testStatus = false;
+        }
+
+        if (2 != psTraceGetLevel("dummyTraceFunc02")) {
+            printf("TEST ERROR: failed to properly set tracelevel for dummyTraceFunc02\n");
+            printf("    tracelevel was %d, should be 2.\n", psTraceGetLevel("dummyTraceFunc02"));
+            testStatus = false;
+        }
+
+        if (1 != psLogGetDestination()) {
+            printf("TEST ERROR: failed to properly set log destination.\n");
+            printf("    it was %d, should be 1\n", psLogGetDestination());
+            testStatus = false;
+        }
+
+        if (3 != psLogGetLevel()) {
+            printf("TEST ERROR: failed to properly set log level.\n");
+            printf("    it was %d, should be 3\n", psLogGetLevel());
+            testStatus = false;
+        }
+
+        //
+        // Test the several arbitrary config file strings.
+        //
+        psS32 tmpInt = psMetadataLookupS32(&rc, site, "ARBITRARY_STRING_S32");
+        if ((rc == false) || (tmpInt != 20)) {
+            printf("TEST ERROR: failed to properly set metadata integer ARBITRARY_STRING_S32.\n");
+            testStatus = false;
+        }
+
+        psF32 tmpFloat = psMetadataLookupS32(&rc, site, "ARBITRARY_STRING_F32");
+        if ((rc == false) || (tmpFloat != 21.0)) {
+            printf("TEST ERROR: failed to properly set metadata float ARBITRARY_STRING_F32.\n");
+            testStatus = false;
+        }
+
+        psF64 tmpDub = psMetadataLookupS32(&rc, site, "ARBITRARY_STRING_F64");
+        if ((rc == false) || (tmpDub != 22.0)) {
+            printf("TEST ERROR: failed to properly set metadata double ARBITRARY_STRING_F64.\n");
+            testStatus = false;
+        }
+
+        psBool tmpBool;
+        tmpBool = psMetadataLookupS32(&rc, site, "ARBITRARY_STRING_BOOL_T");
+        if ((rc == false) || (tmpBool != true)) {
+            printf("TEST ERROR: failed to properly set metadata double ARBITRARY_STRING_BOOL_T.\n");
+            testStatus = false;
+        }
+        tmpBool = psMetadataLookupS32(&rc, site, "ARBITRARY_STRING_BOOL_F");
+        if ((rc == false) || (tmpBool != false)) {
+            printf("TEST ERROR: failed to properly set metadata double ARBITRARY_STRING_BOOL_F.\n");
+            testStatus = false;
+        }
+
+        //
+        // Test the database camera metadata keywords.
+        //
+        psMetadata *tmpMeta = psMetadataLookupMetadata(&rc, site, "CAMERAS");
+        if (rc == false) {
+            printf("TEST ERROR: failed to properly set metadata metadata CAMERAS.\n");
+            testStatus = false;
+        } else {
+            psString tmpStr;
+            tmpStr = psMetadataLookupStr(&rc, tmpMeta, "MEGACAM_RAW");
+            if ((rc == false) || (0 != strcmp(tmpStr, "megacam_raw.config"))) {
+                printf("TEST ERROR: failed to properly set metadata metadata CAMERAS->MEGACAM_RAW.\n");
+                testStatus = false;
+            }
+
+            tmpStr = psMetadataLookupStr(&rc, tmpMeta, "MEGACAM_SPLICE");
+            if ((rc == false) || (0 != strcmp(tmpStr, "megacam_splice.config"))) {
+                printf("TEST ERROR: failed to properly set metadata metadata CAMERAS->MEGACAM_SPLICE.\n");
+                testStatus = false;
+            }
+
+            tmpStr = psMetadataLookupStr(&rc, tmpMeta, "GPC1_RAW");
+            if ((rc == false) || (0 != strcmp(tmpStr, "gpc1_raw.config"))) {
+                printf("TEST ERROR: failed to properly set metadata metadata CAMERAS->GPC1_RAW.\n");
+                testStatus = false;
+            }
+
+            tmpStr = psMetadataLookupStr(&rc, tmpMeta, "LRIS_BLUE");
+            if ((rc == false) || (0 != strcmp(tmpStr, "lris_blue.config"))) {
+                printf("TEST ERROR: failed to properly set metadata metadata CAMERAS->LRIS_BLUE.\n");
+                testStatus = false;
+            }
+
+            tmpStr = psMetadataLookupStr(&rc, tmpMeta, "LRIS_RED");
+            if ((rc == false) || (0 != strcmp(tmpStr, "lris_red.config"))) {
+                printf("TEST ERROR: failed to properly set metadata metadata CAMERAS->LRIS_RED.\n");
+                testStatus = false;
+            }
+        }
+
+        //
+        // Test the database metadata keywords.  This is somewhat redundant since
+        // we already test metadat strings.
+        //
+        psString tmpStr;
+        tmpStr = psMetadataLookupStr(&rc, site, "DBSERVER");
+        if ((rc == false) || (0 != strcmp(tmpStr, "ippdb.ifa.hawaii.edu"))) {
+            printf("TEST ERROR: failed to properly set metadata string DBSERVER.\n");
+            testStatus = false;
+        }
+
+        tmpStr = psMetadataLookupStr(&rc, site, "DBUSER");
+        if ((rc == false) || (0 != strcmp(tmpStr, "ipp"))) {
+            printf("TEST ERROR: failed to properly set metadata string DBUSER.\n");
+            testStatus = false;
+        }
+
+        tmpStr = psMetadataLookupStr(&rc, site, "DBPASSWORD");
+        if ((rc == false) || (0 != strcmp(tmpStr, "password"))) {
+            printf("TEST ERROR: failed to properly set metadata string DBPASSWORD.\n");
+            testStatus = false;
+        }
+    }
+
+    psFree(site);
+    psFree(camera);
+    psFree(recipe);
+    return(testStatus);
+}
Index: /branches/eam_branch_20071023/psModules/test/detrend/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/.cvsignore	(revision 15358)
@@ -0,0 +1,9 @@
+.deps
+.libs
+Makefile
+Makefile.in
+temp
+tst_pmFlatField
+tst_pmMaskBadPixels
+tst_pmNonLinear
+.tmp_tst_pmNonLinearLookupFile
Index: /branches/eam_branch_20071023/psModules/test/detrend/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/Makefile.am	(revision 15358)
@@ -0,0 +1,26 @@
+AM_CPPFLAGS = \
+	$(SRCINC) \
+	-I$(top_srcdir)/test/tap/src \
+	-I$(top_srcdir)/test/pstap/src \
+	$(PSMODULES_CFLAGS)
+
+AM_LDFLAGS = \
+	$(top_builddir)/src/libpsmodules.la  \
+	$(top_builddir)/test/tap/src/libtap.la \
+	$(top_builddir)/test/pstap/src/libpstap.la \
+	$(PSMODULES_LIBS)
+
+TEST_PROGS = \
+	tap_pmShutterCorrection
+
+if BUILD_TESTS
+bin_PROGRAMS = $(TEST_PROGS)
+TESTS = $(TEST_PROGS)
+else
+check_PROGRAMS = $(TEST_PROGS)
+endif
+
+CLEANFILES = $(check_DATA) temp/* core core.* *~ *.bb *.bbg *.da gmon.out
+
+test: check
+	$(top_srcdir)/test/test.pl
Index: /branches/eam_branch_20071023/psModules/test/detrend/tap_pmBias.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/tap_pmBias.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/tap_pmBias.c	(revision 15358)
@@ -0,0 +1,529 @@
+/** @file tap_pmBias.c
+ *
+ * XXX: pmBiasSubtractFrame(): Must add tests for CELL.X0, CELL.Y0 stuff:
+ * XXX: pmBiasSubtractFrame(): Must add tests with scale != 1.0
+ * XXX: pmBiasSubtract(): must test with acceptable data
+ *
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+#define BADFLAT			0
+#define VERBOSE                 1
+#define ERR_TRACE_LEVEL         0
+#define TEST_NUM_ROWS		8
+#define TEST_NUM_COLS		8
+#define NUM_BIAS_DATA		2
+#define NUM_HDUS		8
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define CELL_ALLOC_NAME        "CellName"
+#define NUM_READOUTS            3
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (int i=0;i<TEST_NUM_ROWS;i++) {
+        for (int j=0;j<TEST_NUM_COLS;j++) {
+            readout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../camera/data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(1);
+
+
+    // ----------------------------------------------------------------------
+    // pmBiasSubtractFrame() tests
+    // bool pmBiasSubtractFrame(pmReadout *in, pmReadout *sub, float scale)
+    // Call pmBiasSubtractFrame() with NULL pmReadout input
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *sub = generateSimpleReadout(cell);
+
+        ok(!pmBiasSubtractFrame(NULL, sub, 1.0), "pmBiasSubtractFrame(NULL, sub, 1.0) returned FALSE");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // bool pmBiasSubtractFrame(pmReadout *in, pmReadout *sub, float scale)
+    // Call pmBiasSubtractFrame() with NULL pmReadout input->image
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->image);
+        in->image = NULL;
+        pmReadout *sub = generateSimpleReadout(cell);
+
+        ok(!pmBiasSubtractFrame(in, sub, 1.0), "pmBiasSubtractFrame() returned FALSE with NULL pmReadout input->image");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtractFrame() with empty pmReadout input->image
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->image);
+        in->image = psImageAlloc(0, 0, PS_TYPE_F32);
+        pmReadout *sub = generateSimpleReadout(cell);
+
+        ok(!pmBiasSubtractFrame(in, sub, 1.0), "pmBiasSubtractFrame() returned FALSE with empty input->image");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtractFrame() with bad type for pmReadout input->image
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->image);
+        in->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmReadout *sub = generateSimpleReadout(cell);
+
+        ok(!pmBiasSubtractFrame(in, sub, 1.0), "pmBiasSubtractFrame() returned FALSE with bad type for pmReadout input->image");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtractFrame() with NULL pmReadout sub
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *sub = generateSimpleReadout(cell);
+
+        ok(!pmBiasSubtractFrame(in, NULL, 1.0), "pmBiasSubtractFrame(in, NULL, 1.0) returned FALSE");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtractFrame() with NULL pmReadout sub->image
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *sub = generateSimpleReadout(cell);
+        psFree(sub->image);
+        sub->image = NULL;
+
+        ok(!pmBiasSubtractFrame(in, sub, 1.0), "pmBiasSubtractFrame() returned FALSE with NULL pmReadout sub->image");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtractFrame() with empty pmReadout sub->image
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *sub = generateSimpleReadout(cell);
+        psFree(sub->image);
+        sub->image = psImageAlloc(0, 0, PS_TYPE_F32);
+
+        ok(!pmBiasSubtractFrame(in, sub, 1.0), "pmBiasSubtractFrame() returned FALSE with NULL pmReadout sub->image");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtractFrame() with bad type for pmReadout sub->image
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *sub = generateSimpleReadout(cell);
+        psFree(sub->image);
+        sub->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+
+        ok(!pmBiasSubtractFrame(in, sub, 1.0), "pmBiasSubtractFrame() returned FALSE with bad type for pmReadout sub->image");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtractFrame() with differing input image sizes
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *sub = generateSimpleReadout(cell);
+        psFree(sub->image);
+        sub->image = psImageAlloc(TEST_NUM_COLS/2, TEST_NUM_ROWS/2, PS_TYPE_S32);
+
+        ok(!pmBiasSubtractFrame(in, sub, 1.0), "pmBiasSubtractFrame() returned FALSE with with differing input image sizes");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtractFrame() with differing image X parity
+    {
+        psMemId id = psMemGetId();
+        pmCell *cellIn = generateSimpleCell(NULL);
+        pmCell *cellSub = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cellIn);
+        pmReadout *sub = generateSimpleReadout(cellSub);
+
+        // Set the X parity differently for each pmReadout's parent cell
+        psMetadataAddS32(in->parent->concepts, PS_LIST_TAIL, "CELL.XPARITY", PS_META_REPLACE, "", -1);
+        psMetadataAddS32(sub->parent->concepts, PS_LIST_TAIL, "CELL.XPARITY", PS_META_REPLACE, "", 1);
+        ok(!pmBiasSubtractFrame(in, sub, 1.0), "pmBiasSubtractFrame() returned FALSE with differing image X parity");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cellIn);
+        myFreeCell(cellSub);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtractFrame() with differing image Y parity
+    {
+        psMemId id = psMemGetId();
+        pmCell *cellIn = generateSimpleCell(NULL);
+        pmCell *cellSub = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cellIn);
+        pmReadout *sub = generateSimpleReadout(cellSub);
+
+        // Set the Y parity differently for each pmReadout's parent cell
+        psMetadataAddS32(in->parent->concepts, PS_LIST_TAIL, "CELL.YPARITY", PS_META_REPLACE, "", -1);
+        psMetadataAddS32(sub->parent->concepts, PS_LIST_TAIL, "CELL.YPARITY", PS_META_REPLACE, "", 1);
+        ok(!pmBiasSubtractFrame(in, sub, 1.0), "pmBiasSubtractFrame() returned FALSE with differing image Y parity");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cellIn);
+        myFreeCell(cellSub);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtractFrame() with differing image Y parity
+    {
+        psMemId id = psMemGetId();
+        pmCell *cellIn = generateSimpleCell(NULL);
+        pmCell *cellSub = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cellIn);
+        pmReadout *sub = generateSimpleReadout(cellSub);
+        for (int i = 0 ; i < in->image->numRows ; i++) {
+            for (int j = 0 ; j < in->image->numCols ; j++) {
+                in->image->data.F32[i][j] = (float) (i + j + 10);
+	    }
+	}
+
+        for (int i = 0 ; i < sub->image->numRows ; i++) {
+            for (int j = 0 ; j < sub->image->numCols ; j++) {
+                sub->image->data.F32[i][j] = 10.0;
+	    }
+	}
+
+        ok(pmBiasSubtractFrame(in, sub, 1.0), "pmBiasSubtractFrame() returned TRUE");
+        bool errorFlag = false;
+        for (int i = 0 ; i < in->image->numRows ; i++) {
+            for (int j = 0 ; j < in->image->numCols ; j++) {
+                if (in->image->data.F32[i][j] != (float) (i + j)) {
+                    if (VERBOSE) diag("ERROR: image[%d][%d] is %.2f, should be %.2f\n",
+                        i, j, in->image->data.F32[i][j], (float) (i + j));
+                    errorFlag = true;
+		}
+	    }
+	}
+
+        ok(!errorFlag, "pmBiasSubtractFrame() set the pixels correctly");
+        psFree(in);
+        psFree(sub);
+        myFreeCell(cellIn);
+        myFreeCell(cellSub);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmBiasSubtract() tests
+    // bool pmBiasSubtract(pmReadout *in, pmOverscanOptions *overscanOpts,
+    //               const pmReadout *bias, const pmReadout *dark, const pmFPAview *view)
+    // Call pmBiasSubtract() with NULL pmReadout input
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *bias = generateSimpleReadout(cell);
+        pmReadout *dark = generateSimpleReadout(cell);
+        pmFPAview *view = pmFPAviewAlloc(TEST_NUM_ROWS);
+        // XXX: Must set the following:
+        pmOverscanOptions *overscanOpts = NULL;
+
+        ok(!pmBiasSubtract(NULL, overscanOpts, bias, dark, view), "pmBiasSubtract() returned FALSE with NULL input pmReadout: in");
+        psFree(in);
+        psFree(bias);
+        psFree(dark);
+        myFreeCell(cell);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtract() with NULL pmReadout input->image
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->image);
+        in->image = NULL;
+        pmReadout *bias = generateSimpleReadout(cell);
+        pmReadout *dark = generateSimpleReadout(cell);
+        pmFPAview *view = pmFPAviewAlloc(TEST_NUM_ROWS);
+        // XXX: Must set the following:
+        pmOverscanOptions *overscanOpts = NULL;
+
+        ok(!pmBiasSubtract(in, overscanOpts, bias, dark, view), "pmBiasSubtract() returned FALSE with NULL input image");
+        psFree(in);
+        psFree(bias);
+        psFree(dark);
+        myFreeCell(cell);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtract() with wrong type for pmReadout input->image
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->image);
+        in->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmReadout *bias = generateSimpleReadout(cell);
+        pmReadout *dark = generateSimpleReadout(cell);
+        pmFPAview *view = pmFPAviewAlloc(TEST_NUM_ROWS);
+        // XXX: Must set the following:
+        pmOverscanOptions *overscanOpts = NULL;
+
+        ok(!pmBiasSubtract(in, overscanOpts, bias, dark, view), "pmBiasSubtract() returned FALSE with wrong type for pmReadout input->image");
+        psFree(in);
+        psFree(bias);
+        psFree(dark);
+        myFreeCell(cell);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtract() with NULL image for input pmReadout bias
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *bias = generateSimpleReadout(cell);
+        psFree(bias->image);
+        bias->image = NULL;
+        pmReadout *dark = generateSimpleReadout(cell);
+        pmFPAview *view = pmFPAviewAlloc(TEST_NUM_ROWS);
+        // XXX: Must set the following:
+        pmOverscanOptions *overscanOpts = NULL;
+
+        ok(!pmBiasSubtract(in, overscanOpts, bias, dark, view), "pmBiasSubtract() returned FALSE with NULL image for input pmReadout bias");
+        psFree(in);
+        psFree(bias);
+        psFree(dark);
+        myFreeCell(cell);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtract() with wrong type image for input pmReadout bias
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *bias = generateSimpleReadout(cell);
+        psFree(bias->image);
+        bias->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmReadout *dark = generateSimpleReadout(cell);
+        pmFPAview *view = pmFPAviewAlloc(TEST_NUM_ROWS);
+        // XXX: Must set the following:
+        pmOverscanOptions *overscanOpts = NULL;
+
+        ok(!pmBiasSubtract(in, overscanOpts, bias, dark, view), "pmBiasSubtract() returned FALSE with wrong type image for input pmReadout bias");
+        psFree(in);
+        psFree(bias);
+        psFree(dark);
+        myFreeCell(cell);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtract() with NULL image for input pmReadout dark
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *bias = generateSimpleReadout(cell);
+        pmReadout *dark = generateSimpleReadout(cell);
+        psFree(dark->image);
+        dark->image = NULL;
+        pmFPAview *view = pmFPAviewAlloc(TEST_NUM_ROWS);
+        // XXX: Must set the following:
+        pmOverscanOptions *overscanOpts = NULL;
+
+        ok(!pmBiasSubtract(in, overscanOpts, bias, dark, view), "pmBiasSubtract() returned FALSE with wrong type image for input pmReadout bias");
+        psFree(in);
+        psFree(bias);
+        psFree(dark);
+        myFreeCell(cell);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtract() with wrong type image for input pmReadout dark
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *bias = generateSimpleReadout(cell);
+        pmReadout *dark = generateSimpleReadout(cell);
+        psFree(dark->image);
+        dark->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmFPAview *view = pmFPAviewAlloc(TEST_NUM_ROWS);
+        // XXX: Must set the following:
+        pmOverscanOptions *overscanOpts = NULL;
+
+        ok(!pmBiasSubtract(in, overscanOpts, bias, dark, view), "pmBiasSubtract() returned FALSE with wrong type image for input pmReadout dark");
+        psFree(in);
+        psFree(bias);
+        psFree(dark);
+        myFreeCell(cell);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmBiasSubtract() with NULL pmView (while pmReadout dark non-NULL)
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *bias = generateSimpleReadout(cell);
+        pmReadout *dark = generateSimpleReadout(cell);
+        pmFPAview *view = pmFPAviewAlloc(TEST_NUM_ROWS);
+        // XXX: Must set the following:
+        pmOverscanOptions *overscanOpts = NULL;
+
+        ok(!pmBiasSubtract(in, overscanOpts, bias, dark, NULL), "pmBiasSubtract() returned FALSE with NULL pmView (while pmReadout dark non-NULL)");
+        psFree(in);
+        psFree(bias);
+        psFree(dark);
+        myFreeCell(cell);
+        psFree(view);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branch_20071023/psModules/test/detrend/tap_pmFlatField.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/tap_pmFlatField.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/tap_pmFlatField.c	(revision 15358)
@@ -0,0 +1,348 @@
+/** @file tap_pmFlatField.c
+ *
+ * XXX: Added a mask argument to pmFlatField().  Must add tests.  For now, all
+ * masks are NULL.
+ *
+ *  XXX: I added the CELL.TRIMSEC region code but there are not tests for it.
+ *
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+#define BADFLAT			0
+#define VERBOSE                 1
+#define ERR_TRACE_LEVEL         10
+#define TEST_NUM_ROWS		8
+#define TEST_NUM_COLS		8
+#define NUM_BIAS_DATA		2
+#define NUM_HDUS		8
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define CELL_ALLOC_NAME        "CellName"
+#define NUM_READOUTS            3
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (int i=0;i<TEST_NUM_ROWS;i++) {
+        for (int j=0;j<TEST_NUM_COLS;j++) {
+            readout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../camera/data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    plan_tests(28);
+
+
+    // bool pmFlatField(pmReadout *in, pmReadout *flat, psMaskType badFlat)
+    // Test pmFlatField() with NULL input psReadout
+    {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        pmReadout *flat = generateSimpleReadout(NULL);
+        ok(!pmFlatField(NULL, flat, 0), "pmFlatField(NULL, flat, 0) returned FALSE");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // bool pmFlatField(pmReadout *in, pmReadout *flat, psMaskType badFlat)
+    // Test pmFlatField() with NULL input psReadout->image
+    {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        psFree(in->image);
+        in->image = NULL;
+        pmReadout *flat = generateSimpleReadout(NULL);
+        ok(!pmFlatField(in, flat, 0), "pmFlatField(in, flat, 0) returned FALSE with NULL input psReadout->image");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // bool pmFlatField(pmReadout *in, pmReadout *flat, psMaskType badFlat)
+    // Test pmFlatField() with NULL input psReadout->image
+    {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        psFree(in->image);
+        in->image = psImageAlloc(0, 0, PS_TYPE_F32);
+        pmReadout *flat = generateSimpleReadout(NULL);
+        ok(!pmFlatField(in, flat, 0), "pmFlatField(in, flat, 0) returned FALSE with empty input psReadout->image");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // bool pmFlatField(pmReadout *in, pmReadout *flat, psMaskType badFlat)
+    // Test pmFlatField() with NULL input flat
+    {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        pmReadout *flat = generateSimpleReadout(NULL);
+        ok(!pmFlatField(in, NULL, 0), "pmFlatField(in, NULL, 0) returned FALSE");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // bool pmFlatField(pmReadout *in, pmReadout *flat, psMaskType badFlat)
+    // Test pmFlatField() with NULL input flat->image
+    {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        pmReadout *flat = generateSimpleReadout(NULL);
+        psFree(flat->image);
+        flat->image = NULL;
+        ok(!pmFlatField(in, NULL, 0), "pmFlatField(in, flat, 0) returned FALSE with NULL input flat->image");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // bool pmFlatField(pmReadout *in, pmReadout *flat, psMaskType badFlat)
+    // Test pmFlatField() with NULL input flat->image
+    {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        pmReadout *flat = generateSimpleReadout(NULL);
+        psFree(flat->image);
+        flat->image = psImageAlloc(0, 0, PS_TYPE_F32);
+        ok(!pmFlatField(in, NULL, 0), "pmFlatField(in, flat, 0) returned FALSE with empty input flat->image");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // bool pmFlatField(pmReadout *in, pmReadout *flat, psMaskType badFlat)
+    // Test pmFlatField() with differing types of flat/in image
+    {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        pmReadout *flat = generateSimpleReadout(NULL);
+        psFree(flat->image);
+        flat->image = psImageAlloc(0, 0, PS_TYPE_F64);
+        ok(!pmFlatField(in, NULL, 0), "pmFlatField(in, flat, 0) returned FALSE with differing types of flat/in image");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmFlatField() with input image larger than flat image
+    {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        pmReadout *flat = generateSimpleReadout(NULL);
+        psFree(flat->image);
+        flat->image = psImageAlloc(TEST_NUM_ROWS/2, TEST_NUM_COLS/2, PS_TYPE_F32);
+        ok(!pmFlatField(in, flat, 1), "pmFlatField(in, flat, 0) returned FALSE with input image larger than flat image");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmFlatField() with input image and flat image of differing types
+    {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        pmReadout *flat = generateSimpleReadout(NULL);
+        psFree(flat->image);
+        flat->image = psImageAlloc(TEST_NUM_ROWS, TEST_NUM_COLS, PS_TYPE_F64);
+        ok(!pmFlatField(in, flat, 1), "pmFlatField(in, flat, 0) returned FALSE with input image and flat image of differing types");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmFlatField() with input image mask smaller than input image
+    {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        pmReadout *flat = generateSimpleReadout(NULL);
+        psFree(in->mask);
+        in->mask = psImageAlloc(TEST_NUM_ROWS/2, TEST_NUM_COLS/2, PS_TYPE_MASK);
+        ok(!pmFlatField(in, flat, 1), "pmFlatField(in, flat, 0) returned FALSE with input image mask smaller than input image");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmFlatField() with input image mask of incorrect type
+    {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        pmReadout *flat = generateSimpleReadout(NULL);
+        psFree(in->mask);
+        in->mask = psImageAlloc(TEST_NUM_ROWS, TEST_NUM_COLS, PS_TYPE_F32);
+        ok(!pmFlatField(in, flat, 1), "pmFlatField(in, flat, 0) returned FALSE with input image mask of incorrect type");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmFlatField() with offset greater than input image
+    // XXX: Must rewrite with offsets coming from metadata
+    if (0) {
+        psMemId id = psMemGetId();
+        pmReadout *in = generateSimpleReadout(NULL);
+        pmReadout *flat = generateSimpleReadout(NULL);
+        *(int*)&in->col0 = 50;
+        *(int*)&in->row0 = 50;
+        ok(!pmFlatField(in, flat, 1), "pmFlatField(in, flat, 0) returned FALSE with offset greater than input image");
+        psFree(in);
+        psFree(flat);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmFlatField() with acceptable input data
+    // Set the flat field to 1
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *flat = generateSimpleReadout(cell);
+        psImageInit(in->image, 6.0);
+        psImageInit(in->mask, 0);
+        psImageInit(flat->image, 2.0);
+        ok(pmFlatField(in, flat, 1), "pmFlatField(in, flat, 0) returned TRUE with acceptable input data");
+        bool errorFlag = false;
+        for (int i = 0 ; i < in->image->numRows ; i++) {
+            for (int j = 0 ; j < in->image->numCols ; j++) {
+                if (in->image->data.F32[i][j] != 3.0) {
+                    if (VERBOSE) diag("ERROR: image[%d][%d] is %.2f, should be 3.0", i, j,
+                        in->image->data.F32[i][j]);
+		}
+                if (in->mask->data.PS_TYPE_MASK_DATA[i][j] != 0) {
+                    if (VERBOSE) diag("ERROR: mask[%d][%d] is %d, should be 0", i, j,
+                        in->image->data.PS_TYPE_MASK_DATA[i][j]);
+		}
+	    }
+	}
+        ok(!errorFlag, "pmFlatField() set the image data correctly");
+
+        psFree(in);
+        psFree(flat);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmFlatField() with acceptable input data
+    // Set the flat field to -1
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *flat = generateSimpleReadout(cell);
+        psImageInit(in->image, 6.0);
+        psImageInit(in->mask, 0);
+        psImageInit(flat->image, -1.0);
+        ok(pmFlatField(in, flat, 1), "pmFlatField(in, flat, 0) returned TRUE with acceptable input data");
+        bool errorFlag = false;
+        for (int i = 0 ; i < in->image->numRows ; i++) {
+            for (int j = 0 ; j < in->image->numCols ; j++) {
+                if (!isnan(in->image->data.F32[i][j])) {
+                    if (VERBOSE) diag("ERROR: image[%d][%d] is %.2f, should be NAN", i, j,
+                        in->image->data.F32[i][j]);
+		}
+                if (in->mask->data.PS_TYPE_MASK_DATA[i][j] != 1) {
+                    if (VERBOSE) diag("ERROR: mask[%d][%d] is %d, should be 0", i, j,
+                        in->image->data.PS_TYPE_MASK_DATA[i][j]);
+		}
+	    }
+	}
+        ok(!errorFlag, "pmFlatField() set the image data correctly");
+
+        psFree(in);
+        psFree(flat);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branch_20071023/psModules/test/detrend/tap_pmMaskBadPixels.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/tap_pmMaskBadPixels.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/tap_pmMaskBadPixels.c	(revision 15358)
@@ -0,0 +1,520 @@
+/** @file tap_pmMaskBadPixels.c
+ *
+ * XXX: must test pmMaskFlagSuspectPixels() with acceptable input data
+ * XXX: must test pmMaskIdentifyBadPixels() with acceptable input data
+ *
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+#define BADFLAT			0
+#define VERBOSE                 1
+#define ERR_TRACE_LEVEL         0
+#define TEST_NUM_ROWS		8
+#define TEST_NUM_COLS		8
+#define NUM_BIAS_DATA		2
+#define NUM_HDUS		8
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define CELL_ALLOC_NAME        "CellName"
+#define NUM_READOUTS            3
+#define BASE_IMAGE              10
+#define BASE_MASK               40
+#define BASE_WEIGHT             70
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (int i=0;i<TEST_NUM_ROWS;i++) {
+        for (int j=0;j<TEST_NUM_COLS;j++) {
+            readout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+/******************************************************************************
+generateSimpleCell(): This function generates a pmCell data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmCell *generateSimpleCell(pmChip *chip)
+{
+    pmCell *cell = pmCellAlloc(chip, CELL_ALLOC_NAME);
+
+    psMetadataAddS32(cell->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psMetadataAddS32(cell->concepts, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    psArrayRealloc(cell->readouts, NUM_READOUTS);
+    cell->hdu = pmHDUAlloc("cellExtName");
+    for (int i = 0 ; i < NUM_READOUTS ; i++) {
+        cell->readouts->data[i] = generateSimpleReadout(cell);
+    }
+
+    bool rc = pmConfigFileRead(&cell->hdu->format, "../camera/data/camera0/format0.config", "Camera format 0");
+    if (!rc) {
+        diag("pmConfigFileRead() was unsuccessful (from generateSimpleCell())");
+    }
+
+    cell->hdu->images = psArrayAlloc(NUM_HDUS);
+    cell->hdu->masks = psArrayAlloc(NUM_HDUS);
+    cell->hdu->weights = psArrayAlloc(NUM_HDUS);
+    for (int k = 0 ; k < NUM_HDUS ; k++) {
+        cell->hdu->images->data[k]  = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        cell->hdu->masks->data[k]   = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_MASK);
+        cell->hdu->weights->data[k] = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(cell->hdu->images->data[k], (float) (BASE_IMAGE+k));
+        psImageInit(cell->hdu->masks->data[k], (psU8) (BASE_MASK+k));
+        psImageInit(cell->hdu->weights->data[k], (float) (BASE_WEIGHT+k));
+    }
+
+    //XXX: Should the region be set some other way?  Like through the various config files?
+//    psRegion *region = psRegionAlloc(0.0, TEST_NUM_COLS-1, 0.0, TEST_NUM_ROWS-1);
+    psRegion *region = psRegionAlloc(0.0, 0.0, 0.0, 0.0);
+    // You shouldn't have to remove the key from the metadata.  Find out how to simply change the key value.
+    psMetadataRemoveKey(cell->concepts, "CELL.TRIMSEC");
+    psMetadataAddPtr(cell->concepts, PS_LIST_TAIL|PS_META_REPLACE, "CELL.TRIMSEC", PS_DATA_REGION, "I am a region", region);
+    psFree(region);
+    return(cell);
+}
+
+void myFreeCell(pmCell *cell)
+{
+    for (int k = 0 ; k < cell->readouts->n ; k++) {
+        psFree(cell->readouts->data[k]);
+    }
+    psFree(cell);
+}
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(1);
+
+
+    // ----------------------------------------------------------------------
+    // pmMaskBadPixels() tests
+    // Call pmMaskBadPixels() with NULL pmReadout input
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *mask = generateSimpleReadout(cell);
+
+        ok(!pmMaskBadPixels(NULL, mask, 0), "pmMaskBadPixels(NULL, mask, 0) returned FALSE");
+        psFree(in);
+        psFree(mask);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskBadPixels() with NULL pmReadout input->mask
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->mask);
+        in->mask = NULL;
+        pmReadout *mask = generateSimpleReadout(cell);
+
+        ok(!pmMaskBadPixels(in, mask, 0), "pmMaskBadPixels() returned FALSE with NULL pmReadout input->mask");
+        psFree(in);
+        psFree(mask);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskBadPixels() with incorrect type for input->mask
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->mask);
+        in->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        pmReadout *mask = generateSimpleReadout(cell);
+
+        ok(!pmMaskBadPixels(in, mask, 0), "pmMaskBadPixels() returned FALSE with NULL pmReadout input->mask");
+        psFree(in);
+        psFree(mask);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskBadPixels() with NULL pmReadout mask
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *mask = generateSimpleReadout(cell);
+
+        ok(!pmMaskBadPixels(in, NULL, 0), "pmMaskBadPixels(in, NULL, 0) returned FALSE");
+        psFree(in);
+        psFree(mask);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskBadPixels() with NULL pmReadout mask->mask
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *mask = generateSimpleReadout(cell);
+        psFree(mask->mask);
+        mask->mask = NULL;
+
+        ok(!pmMaskBadPixels(in, mask, 0), "pmMaskBadPixels() returned FALSE with NULL pmReadout mask->mask");
+        psFree(in);
+        psFree(mask);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskBadPixels() with incorrect type for mask->mask
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *mask = generateSimpleReadout(cell);
+        psFree(mask->mask);
+        mask->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+
+        ok(!pmMaskBadPixels(in, mask, 0), "pmMaskBadPixels() returned FALSE with incorrect type for mask->mask");
+        psFree(in);
+        psFree(mask);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskBadPixels() with input mask larger than mask->mask
+    {
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *mask = generateSimpleReadout(cell);
+        psFree(mask->mask);
+        mask->mask = psImageAlloc(TEST_NUM_COLS/2, TEST_NUM_ROWS/2, PS_TYPE_MASK);
+
+        ok(!pmMaskBadPixels(in, mask, 0), "pmMaskBadPixels() returned FALSE with input mask larger than mask->mask");
+        psFree(in);
+        psFree(mask);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskBadPixels() with acceptable input data
+    {
+        #define MASK_VAL 1
+        psMemId id = psMemGetId();
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        pmReadout *mask = generateSimpleReadout(cell);
+        for (int i = 0 ; i < in->mask->numRows ; i++) {
+            for (int j = 0 ; j < in->mask->numCols ; j++) {
+                in->mask->data.PS_TYPE_MASK_DATA[i][j] = 0;
+                mask->mask->data.PS_TYPE_MASK_DATA[i][j] = 0;
+                if (i >= in->mask->numRows/2 && j >= in->mask->numCols/2) {
+                    mask->mask->data.PS_TYPE_MASK_DATA[i][j] = MASK_VAL;
+		}
+	    }
+	}
+
+        ok(pmMaskBadPixels(in, mask, MASK_VAL), "pmMaskBadPixels() returned TRUE with acceptable input data");
+        bool errorFlag = false;     
+        for (int i = 0 ; i < in->mask->numRows ; i++) {
+            for (int j = 0 ; j < in->mask->numCols ; j++) {
+                psU8 expect;
+                if (i >= in->mask->numRows/2 && j >= in->mask->numCols/2) {
+                    expect = MASK_VAL;
+		} else {
+                    expect = 0;
+		}
+                if (in->mask->data.PS_TYPE_MASK_DATA[i][j] != expect) {
+                    if (VERBOSE) diag("ERROR: in->mask[%d][%d] is %d, should be %d",
+                                       i, j, in->mask->data.PS_TYPE_MASK_DATA[i][j], expect);
+                    errorFlag = true;
+		}
+	    }
+	}
+        ok(!errorFlag, "pmMaskBadPixels() set the input mask correctly");
+
+        psFree(in);
+        psFree(mask);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+
+    // ----------------------------------------------------------------------
+    // pmMaskFlagSuspectPixels() tests
+    // Call pmMaskFlagSuspectPixels() with NULL pmReadout input
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+
+        ok(!pmMaskFlagSuspectPixels(out, NULL, 1.0, 1, 1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with NULL input pmReadout");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskFlagSuspectPixels() with NULL pmReadout->image
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->image);
+        in->image = NULL;
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+        ok(!pmMaskFlagSuspectPixels(out, NULL, 1.0, 1, 1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with NULL input pmReadout->image");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskFlagSuspectPixels() with empty pmReadout->image
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->image);
+        in->image = psImageAlloc(0, 0, PS_TYPE_F32);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+        ok(!pmMaskFlagSuspectPixels(out, NULL, 1.0, 1, 1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with empty input pmReadout->image");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskFlagSuspectPixels() with incorrect type for pmReadout->image
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->image);
+        in->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+        ok(!pmMaskFlagSuspectPixels(out, NULL, 1.0, 1, 1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with incorrect type for pmReadout->image");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskFlagSuspectPixels() with empty in->mask
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->mask);
+        in->mask = psImageAlloc(0, 0, PS_TYPE_MASK);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+        ok(!pmMaskFlagSuspectPixels(out, NULL, 1.0, 1, 1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with incorrect type for pmReadout->image");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskFlagSuspectPixels() with bad size for in->mask
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->mask);
+        in->mask = psImageAlloc(TEST_NUM_COLS/2, TEST_NUM_ROWS/2, PS_TYPE_MASK);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+        ok(!pmMaskFlagSuspectPixels(out, NULL, 1.0, 1, 1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with bad size for in->mask");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskFlagSuspectPixels() with bad type for in->mask
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psFree(in->mask);
+        in->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+        ok(!pmMaskFlagSuspectPixels(out, NULL, 1.0, 1, 1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with bad type for in->mask");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskFlagSuspectPixels() with empty out image
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(0, 0, PS_TYPE_S32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+        ok(!pmMaskFlagSuspectPixels(out, NULL, 1.0, 1, 1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with empty out image");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskFlagSuspectPixels() with incorrect type for out image
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+        ok(!pmMaskFlagSuspectPixels(out, NULL, 1.0, 1, 1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with incorrect type for out image");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskFlagSuspectPixels() with incorrect size for out image
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(TEST_NUM_COLS/2, TEST_NUM_ROWS/2, PS_TYPE_S32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+        ok(!pmMaskFlagSuspectPixels(out, NULL, 1.0, 1, 1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with incorrect size for out image");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskFlagSuspectPixels() with rej input <= 0.0
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+
+        ok(!pmMaskFlagSuspectPixels(out, in, 0.0, 1, 1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with rej input <= 0.0");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskFlagSuspectPixels() with frac input outside range [0.0:1.0]
+    {
+        psMemId id = psMemGetId();
+        psImage *out = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        pmCell *cell = generateSimpleCell(NULL);
+        pmReadout *in = generateSimpleReadout(cell);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0);
+
+        ok(!pmMaskFlagSuspectPixels(out, in, 1.0, 1, -1.0, rng), "pmMaskFlagSuspectPixels() returned NULL with frac input < 0.0");
+        ok(!pmMaskFlagSuspectPixels(out, in, 1.0, 1, 2.0, rng), "pmMaskFlagSuspectPixels() returned NULL with frac input > 1.0");
+        psFree(rng);
+        psFree(out);
+        psFree(in);
+        myFreeCell(cell);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+
+
+    // ----------------------------------------------------------------------
+    // pmMaskIdentifyBadPixels() tests
+    // Call pmMaskIdentifyBadPixels() with NULL input image
+    {
+        psMemId id = psMemGetId();
+        psImage *suspects = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_S32);
+        ok(!pmMaskIdentifyBadPixels(NULL, 1.0, 0), "pmMaskIdentifyBadPixels() returned NULL with NULL input image");
+        psFree(suspects);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskIdentifyBadPixels() with empty input image
+    {
+        psMemId id = psMemGetId();
+        psImage *suspects = psImageAlloc(0, 0, PS_TYPE_S32);
+        ok(!pmMaskIdentifyBadPixels(NULL, 1.0, 0), "pmMaskIdentifyBadPixels() returned NULL with empty input image");
+        psFree(suspects);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmMaskIdentifyBadPixels() with incorrect type for input image
+    {
+        psMemId id = psMemGetId();
+        psImage *suspects = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        ok(!pmMaskIdentifyBadPixels(NULL, 1.0, 0), "pmMaskIdentifyBadPixels() returned NULL with incorrect type for input image");
+        psFree(suspects);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+}
+
Index: /branches/eam_branch_20071023/psModules/test/detrend/tap_pmNonLinear.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/tap_pmNonLinear.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/tap_pmNonLinear.c	(revision 15358)
@@ -0,0 +1,237 @@
+/* @file tap_pmNonLinear.c
+ *
+ *  XXX: Add tests 
+ *      Input psVectors and psImages have incorrect type
+ *      Input psVectors are wrong size
+ *
+ */
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+#define TEST_NUM_ROWS 8
+#define TEST_NUM_COLS 8
+#define NUM_BIAS_DATA 2
+#define MISC_NUM                32
+#define MISC_NAME              "META00"
+#define VERBOSE                 0
+#define ERR_TRACE_LEVEL         0
+
+/******************************************************************************
+generateSimpleReadout(): This function generates a pmReadout data structure and then
+populates its members with real data.
+ *****************************************************************************/
+pmReadout *generateSimpleReadout(pmCell *cell)
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+    readout->image = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (int i=0;i<TEST_NUM_ROWS;i++) {
+        for (int j=0;j<TEST_NUM_COLS;j++) {
+            readout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+    readout->mask = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_U8);
+    readout->weight = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < NUM_BIAS_DATA ; i++) {
+        psImage *tmpImage = psImageAlloc(TEST_NUM_COLS, TEST_NUM_ROWS, PS_TYPE_F32);
+        psImageInit(tmpImage, (double) i);
+        psListAdd(readout->bias, PS_LIST_HEAD, tmpImage);
+        psFree(tmpImage);
+    }
+    psMetadataAddS32(readout->analysis, PS_LIST_HEAD, MISC_NAME, 0, NULL, MISC_NUM);
+    return(readout);
+}
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    psTraceSetLevel("err", ERR_TRACE_LEVEL);
+    plan_tests(72);
+
+
+    // ----------------------------------------------------------------------
+    // pmNonLinearityPolynomial() tests
+    // Call pmNonLinearityPolynomial() with NULL input readout.
+    {
+        psMemId id = psMemGetId();
+        psPolynomial1D *myPoly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 2);
+        myPoly->coeff[1] = 1.0;
+        pmReadout *rc = pmNonLinearityPolynomial(NULL, myPoly);
+        ok(!rc, "pmNonLinearityPolynomial() returned NULL with a NULL input pmReadout");
+        psFree(myPoly);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+    
+     
+    // Call pmNonLinearityPolynomial() with NULL input readout->image.
+    {
+        psMemId id = psMemGetId();
+        pmReadout *myReadout = generateSimpleReadout(NULL);
+        psPolynomial1D *myPoly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 2);
+        myPoly->coeff[1] = 1.0;
+        psImage *tmpImage = myReadout->image;
+        myReadout->image = NULL;
+        pmReadout *rc = pmNonLinearityPolynomial(myReadout, myPoly);
+        ok(!rc, "pmNonLinearityPolynomial() returned NULL with a NULL input pmReadout");
+        myReadout->image = tmpImage;
+        psFree(myReadout);
+        psFree(myPoly);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+    
+     
+    // Call pmNonLinearityPolynomial() with NULL polynomial.
+    {
+        psMemId id = psMemGetId();
+        pmReadout *myReadout = generateSimpleReadout(NULL);
+        pmReadout *rc = pmNonLinearityPolynomial(myReadout, NULL);
+        ok(!rc, "pmNonLinearityPolynomial() returned NULL with a NULL input psPolynomial");
+        psFree(myReadout);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+    
+     
+    // Call pmNonLinearityPolynomial() with acceptable input data
+    { 
+        psMemId id = psMemGetId();
+        pmReadout *myReadout = generateSimpleReadout(NULL);
+        psPolynomial1D *myPoly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 2);
+        myPoly->coeff[1] = 1.0;
+        myReadout = pmNonLinearityPolynomial(myReadout, myPoly);
+        bool errorFlag = false;
+        for (int i=0;i<TEST_NUM_ROWS;i++) {
+            for (int j=0;j<TEST_NUM_COLS;j++) {
+                float expect = psPolynomial1DEval(myPoly, (float) (i + j));
+                float actual = myReadout->image->data.F32[i][j];
+                if (FLT_EPSILON < fabs(expect - actual)) {
+                    if (VERBOSE) diag("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                    errorFlag = true;
+                }
+            }
+        }
+        ok(!errorFlag, "pmNonLinearityPolynomial() set the image data correctly");
+    
+        psFree(myReadout);
+        psFree(myPoly);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------------------------------------
+    // pmNonLinearityLookup() tests
+    // Call pmNonLinearityLookup() with NULL input pmReadout.
+    {
+        psMemId id = psMemGetId();
+        psVector *inVec = psVectorAlloc(PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3, PS_TYPE_F32);
+        psVector *outVec = psVectorAlloc(PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3, PS_TYPE_F32);
+        for (psS32 i=0;i<PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3;i++) {
+            inVec->data.F32[i] = (float) i;
+            outVec->data.F32[i] = (float) (2 * i);
+        }
+        pmReadout *rc = pmNonLinearityLookup(NULL, inVec, outVec);
+        ok(!rc, "pmNonLinearityLookup() returned NULL with NULL input pmReadout");
+        psFree(inVec);
+        psFree(outVec);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmNonLinearityLookup() with NULL input pmReadout->image
+    {
+        psMemId id = psMemGetId();
+        pmReadout *myReadout = generateSimpleReadout(NULL);
+        psImage *tmpImage = myReadout->image;
+        myReadout->image = NULL;
+        psVector *inVec = psVectorAlloc(PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3, PS_TYPE_F32);
+        psVector *outVec = psVectorAlloc(PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3, PS_TYPE_F32);
+        for (psS32 i=0;i<PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3;i++) {
+            inVec->data.F32[i] = (float) i;
+            outVec->data.F32[i] = (float) (2 * i);
+        }
+        pmReadout *rc = pmNonLinearityLookup(myReadout, inVec, outVec);
+        ok(!rc, "pmNonLinearityLookup() returned NULL with NULL input pmReadout->image");
+        myReadout->image = tmpImage;
+        psFree(myReadout);
+        psFree(inVec);
+        psFree(outVec);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmNonLinearityLookup() with NULL input inFlux
+    {
+        psMemId id = psMemGetId();
+        pmReadout *myReadout = generateSimpleReadout(NULL);
+        psVector *inVec = psVectorAlloc(PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3, PS_TYPE_F32);
+        psVector *outVec = psVectorAlloc(PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3, PS_TYPE_F32);
+        for (psS32 i=0;i<PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3;i++) {
+            inVec->data.F32[i] = (float) i;
+            outVec->data.F32[i] = (float) (2 * i);
+        }
+        pmReadout *rc = pmNonLinearityLookup(myReadout, NULL, outVec);
+        ok(!rc, "pmNonLinearityLookup() returned NULL with input inFlux");
+        psFree(myReadout);
+        psFree(inVec);
+        psFree(outVec);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Call pmNonLinearityLookup() with NULL input outFlux
+    {
+        psMemId id = psMemGetId();
+        pmReadout *myReadout = generateSimpleReadout(NULL);
+        psVector *inVec = psVectorAlloc(PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3, PS_TYPE_F32);
+        psVector *outVec = psVectorAlloc(PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3, PS_TYPE_F32);
+        for (psS32 i=0;i<PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3;i++) {
+            inVec->data.F32[i] = (float) i;
+            outVec->data.F32[i] = (float) (2 * i);
+        }
+        pmReadout *rc = pmNonLinearityLookup(myReadout, inVec, NULL);
+        ok(!rc, "pmNonLinearityLookup() returned NULL with input outFlux");
+        psFree(myReadout);
+        psFree(inVec);
+        psFree(outVec);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmNonLinearityLookup() with acceptable input data
+    {
+        psMemId id = psMemGetId();
+        pmReadout *myReadout = generateSimpleReadout(NULL);
+        psVector *inVec = psVectorAlloc(PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3, PS_TYPE_F32);
+        psVector *outVec = psVectorAlloc(PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3, PS_TYPE_F32);
+        for (psS32 i=0;i<PS_MAX(TEST_NUM_COLS, TEST_NUM_ROWS)*3;i++) {
+            inVec->data.F32[i] = (float) i;
+            outVec->data.F32[i] = (float) (2 * i);
+        }
+        {
+            myReadout = pmNonLinearityLookup(myReadout, inVec, outVec);
+            ok(myReadout, "pmNonLinearityLookup() returned non-NULL with acceptable input data");
+            bool errorFlag = false;
+            if (myReadout) {
+                for (int i=0;i<TEST_NUM_ROWS;i++) {
+                    for (int j=0;j<TEST_NUM_COLS;j++) {
+                        float expect = (float) (2 * (i + j));
+                        float actual = myReadout->image->data.F32[i][j];
+                        if (FLT_EPSILON < fabs(expect - actual)) {
+                            if (VERBOSE) diag("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                            errorFlag = true;
+                        }
+                    }
+                }
+                ok(!errorFlag, "pmNonLinearityLookup() set the image data correctly");
+	    }
+	}
+        psFree(myReadout);
+        psFree(inVec);
+        psFree(outVec);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
Index: /branches/eam_branch_20071023/psModules/test/detrend/tap_pmShutterCorrection.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/tap_pmShutterCorrection.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/tap_pmShutterCorrection.c	(revision 15358)
@@ -0,0 +1,430 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+
+int main (void)
+{
+    plan_tests(143);
+
+    diag("pmShutterCorrection tests");
+
+    // test allocation, free of pmShutterCorrPars
+    diag("pmShutterCorrParsAlloc tests");
+    {
+        psMemId id = psMemGetId();
+        pmShutterCorrection *pars = pmShutterCorrectionAlloc ();
+
+        ok(pars != NULL, "pmShutterCorrPars successfully allocated");
+        skip_start(pars == NULL, 0, "Skipping tests because pmShutterCorrParsAlloc() failed");
+        skip_end();
+
+        psFree(pars);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // test parameter guess (linearly spaced exptimes, TK/TO < 1)
+    diag("pmShutterCorrectionGuess tests : coarse linear-spaced exptimes");
+    {
+        psMemId id = psMemGetId();
+
+        int NPTS = 10;
+        float AK = 5.0;
+        float TK = 0.1;
+        float TO = 0.2;
+        psVector *exptime = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *counts  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        exptime->n = counts->n = NPTS;
+
+        for (int i = 0; i < exptime->n; i++) {
+            exptime->data.F32[i] = i*0.25;
+            counts->data.F32[i] = AK*(exptime->data.F32[i] + TK) / (exptime->data.F32[i] + TO);
+        }
+
+        pmShutterCorrection *pars = pmShutterCorrectionGuess (exptime, counts);
+
+        ok(pars != NULL, "pmShutterCorrPars successfully allocated");
+        skip_start(pars == NULL, 0, "Skipping tests because pmShutterCorrParsAlloc() failed");
+
+        // with coarse linearly-spaced times large compared to TO and TK,
+        // we can't expect very accurate guesses.  the exptime guess is fairly good because
+        // the largest exptime is much longer than TK or TO
+        ok(fabs(pars->scale  - AK) < 0.5, "scale guess is close enough (got %f vs %f)",  pars->scale, AK);
+        ok(fabs(pars->offset - TK) < 0.5, "offset guess is close enough (got %f vs %f)", pars->offset, TK);
+        ok(fabs(pars->offref - TO) < 0.5, "offref guess is close enough (got %f vs %f)", pars->offref, TO);
+        skip_end();
+
+        psFree(pars);
+        psFree(exptime);
+        psFree(counts);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // test parameter guess (linearly spaced exptimes, TK/TO < 1)
+    diag("pmShutterCorrectionGuess tests : fine linear-spaced exptimes");
+    {
+        psMemId id = psMemGetId();
+
+        int NPTS = 20;
+        float AK = 5.0;
+        float TK = 0.1;
+        float TO = 0.2;
+        psVector *exptime = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *counts  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        exptime->n = counts->n = NPTS;
+
+        for (int i = 0; i < exptime->n; i++) {
+            exptime->data.F32[i] = i*0.1;
+            counts->data.F32[i] = AK*(exptime->data.F32[i] + TK) / (exptime->data.F32[i] + TO);
+        }
+
+        pmShutterCorrection *pars = pmShutterCorrectionGuess (exptime, counts);
+
+        ok(pars != NULL, "pmShutterCorrection successfully allocated");
+        skip_start(pars == NULL, 0, "Skipping tests because pmShutterCorrectionAlloc() failed");
+
+        // with fine linearly-spaced times large compared to TO and TK,
+        // we get a good guess to TK and TO, but since the largest exptime is not
+        // many times larger than TO, we don't do very well with the AK
+        ok(fabs(pars->scale  - AK) < 0.5, "scale guess is close enough (got %f vs %f)",  pars->scale, AK);
+        ok(fabs(pars->offset - TK) < 0.05, "offset guess is close enough (got %f vs %f)", pars->offset, TK);
+        ok(fabs(pars->offref - TO) < 0.05, "offref guess is close enough (got %f vs %f)", pars->offref, TO);
+        skip_end();
+
+        psFree(pars);
+        psFree(exptime);
+        psFree(counts);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // test parameter guess (log spaced exptimes, TK/TO < 1)
+    diag("pmShutterCorrectionGuess tests : log-spaced exptimes");
+    {
+        psMemId id = psMemGetId();
+
+        int NPTS = 40;
+        float AK = 5.0;
+        float TK = 0.1;
+        float TO = 0.2;
+        psVector *exptime = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *counts  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        exptime->n = counts->n = NPTS;
+
+        for (int i = 0; i < exptime->n; i++) {
+            exptime->data.F32[i] = pow(10.0, -2 + i*0.1);
+            counts->data.F32[i] = AK*(exptime->data.F32[i] + TK) / (exptime->data.F32[i] + TO);
+        }
+
+        pmShutterCorrection *pars = pmShutterCorrectionGuess (exptime, counts);
+
+        // with fine log-spaced times well-sampling TO and TK,
+        // we can expect accurate guesses
+        ok(pars != NULL, "pmShutterCorrectionsuccessfully allocated");
+        skip_start(pars == NULL, 0, "Skipping tests because pmShutterCorrectionAlloc() failed");
+        ok(fabs(pars->scale  - AK) < 0.01, "scale guess is close enough (got %f vs %f)",  pars->scale, AK);
+        ok(fabs(pars->offset - TK) < 0.01, "offset guess is close enough (got %f vs %f)", pars->offset, TK);
+        ok(fabs(pars->offref - TO) < 0.01, "offref guess is close enough (got %f vs %f)", pars->offref, TO);
+        skip_end();
+
+        psFree(pars);
+        psFree(exptime);
+        psFree(counts);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // test parameter guess (linearly spaced exptimes, TK/TO > 1)
+    diag("pmShutterCorrectionGuess tests : coarse linear-spaced exptimes, TK/TO > 1");
+    {
+        psMemId id = psMemGetId();
+
+        int NPTS = 10;
+        float AK = 5.0;
+        float TK = 0.2;
+        float TO = 0.1;
+        psVector *exptime = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *counts  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        exptime->n = counts->n = NPTS;
+
+        for (int i = 0; i < exptime->n; i++) {
+            exptime->data.F32[i] = i*0.25;
+            counts->data.F32[i] = AK*(exptime->data.F32[i] + TK) / (exptime->data.F32[i] + TO);
+        }
+
+        pmShutterCorrection *pars = pmShutterCorrectionGuess (exptime, counts);
+
+        ok(pars != NULL, "pmShutterCorrection successfully allocated");
+        skip_start(pars == NULL, 0, "Skipping tests because pmShutterCorrectionAlloc() failed");
+
+        // with coarse linearly-spaced times large compared to TO and TK,
+        // we can't expect very accurate guesses.  the exptime guess is fairly good because
+        // the largest exptime is much longer than TK or TO
+        ok(fabs(pars->scale  - AK) < 0.5, "scale guess is close enough (got %f vs %f)",  pars->scale, AK);
+        ok(fabs(pars->offset - TK) < 0.5, "offset guess is close enough (got %f vs %f)", pars->offset, TK);
+        ok(fabs(pars->offref - TO) < 0.5, "offref guess is close enough (got %f vs %f)", pars->offref, TO);
+        skip_end();
+
+        psFree(pars);
+        psFree(exptime);
+        psFree(counts);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // test parameter guess (linearly spaced exptimes, TK/TO > 1)
+    diag("pmShutterCorrectionGuess tests : fine linear-spaced exptimes, TK/TO > 1");
+    {
+        psMemId id = psMemGetId();
+
+        int NPTS = 20;
+        float AK = 5.0;
+        float TK = 0.2;
+        float TO = 0.1;
+        psVector *exptime = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *counts  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        exptime->n = counts->n = NPTS;
+
+        for (int i = 0; i < exptime->n; i++) {
+            exptime->data.F32[i] = i*0.1;
+            counts->data.F32[i] = AK*(exptime->data.F32[i] + TK) / (exptime->data.F32[i] + TO);
+        }
+
+        pmShutterCorrection *pars = pmShutterCorrectionGuess (exptime, counts);
+
+        ok(pars != NULL, "pmShutterCorrection successfully allocated");
+        skip_start(pars == NULL, 0, "Skipping tests because pmShutterCorrectionsAlloc() failed");
+
+        // with fine linearly-spaced times large compared to TO and TK,
+        // we get a good guess to TK and TO, but since the largest exptime is not
+        // many times larger than TO, we don't do very well with the AK
+        ok(fabs(pars->scale  - AK) < 0.5, "scale guess is close enough (got %f vs %f)",  pars->scale, AK);
+        ok(fabs(pars->offset - TK) < 0.05, "offset guess is close enough (got %f vs %f)", pars->offset, TK);
+        ok(fabs(pars->offref - TO) < 0.05, "offref guess is close enough (got %f vs %f)", pars->offref, TO);
+        skip_end();
+
+        psFree(pars);
+        psFree(exptime);
+        psFree(counts);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // test parameter guess (log spaced exptimes, TK/TO > 1)
+    diag("pmShutterCorrectionGuess tests : log-spaced exptimes, TK/TO > 1");
+    {
+        psMemId id = psMemGetId();
+
+        int NPTS = 40;
+        float AK = 5.0;
+        float TK = 0.2;
+        float TO = 0.1;
+        psVector *exptime = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *counts  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        exptime->n = counts->n = NPTS;
+
+        for (int i = 0; i < exptime->n; i++) {
+            exptime->data.F32[i] = pow(10.0, -2 + i*0.1);
+            counts->data.F32[i] = AK*(exptime->data.F32[i] + TK) / (exptime->data.F32[i] + TO);
+        }
+
+        pmShutterCorrection *pars = pmShutterCorrectionGuess (exptime, counts);
+
+        // with fine log-spaced times well-sampling TO and TK,
+        // we can expect accurate guesses
+        ok(pars != NULL, "pmShutterCorrection successfully allocated");
+        skip_start(pars == NULL, 0, "Skipping tests because pmShutterCorrectionAlloc() failed");
+        ok(fabs(pars->scale  - AK) < 0.01, "scale guess is close enough (got %f vs %f)",  pars->scale, AK);
+        ok(fabs(pars->offset - TK) < 0.01, "offset guess is close enough (got %f vs %f)", pars->offset, TK);
+        ok(fabs(pars->offref - TO) < 0.01, "offref guess is close enough (got %f vs %f)", pars->offref, TO);
+        skip_end();
+
+        psFree(pars);
+        psFree(exptime);
+        psFree(counts);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // test non-linear fitting
+    diag("pmShutterCorrectionFullFit tests : linear-spaced exptimes");
+    {
+        psMemId id = psMemGetId();
+
+        int NPTS = 20;
+        float FL = 10000.0;
+        float AK = 5.0;
+        float TK = 0.2;
+        float TO = 0.1;
+        psVector *exptime = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *counts  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *cntErr  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        exptime->n = counts->n = cntErr->n = NPTS;
+
+        for (int i = 0; i < exptime->n; i++) {
+            exptime->data.F32[i] = i*0.1;
+            counts->data.F32[i] = AK*(exptime->data.F32[i] + TK) / (exptime->data.F32[i] + TO);
+            cntErr->data.F32[i] = AK*sqrt(FL*(exptime->data.F32[i] + TK)) / (exptime->data.F32[i] + TO);
+        }
+
+        pmShutterCorrection *guess = pmShutterCorrectionGuess (exptime, counts);
+        skip_start(guess == NULL, 0, "Skipping tests because pmShutterCorrectionGuess() failed");
+        pmShutterCorrection *pars = pmShutterCorrectionFullFit (exptime, counts, cntErr, guess);
+
+        // with fine log-spaced times well-sampling TO and TK,
+        // we can expect accurate guesses
+        ok(pars != NULL, "pmShutterCorrection successfully allocated by FullFit");
+        skip_start(pars == NULL, 0, "Skipping tests because pmShutterCorrectionsAlloc() failed");
+        ok(fabs(pars->scale  - AK) < 0.01, "scale fit is close enough (got %f vs %f)",  pars->scale, AK);
+        ok(fabs(pars->offset - TK) < 0.01, "offset fit is close enough (got %f vs %f)", pars->offset, TK);
+        ok(fabs(pars->offref - TO) < 0.01, "offref fit is close enough (got %f vs %f)", pars->offref, TO);
+        skip_end();
+
+        psFree(pars);
+        skip_end();
+
+        psFree(guess);
+        psFree(exptime);
+        psFree(counts);
+        psFree(cntErr);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // test non-linear fitting
+    diag("pmShutterCorrectionFullFit tests : log-spaced exptimes");
+    {
+        psMemId id = psMemGetId();
+
+        int NPTS = 40;
+        float FL = 10000.0;
+        float AK = 1.0;
+        float TK = 0.2;
+        float TO = 0.1;
+        psVector *exptime = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *counts  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *cntErr  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        exptime->n = counts->n = cntErr->n = NPTS;
+
+        for (int i = 0; i < exptime->n; i++) {
+            exptime->data.F32[i] = pow(10.0, -2 + i*0.1);
+            counts->data.F32[i] = AK*(exptime->data.F32[i] + TK) / (exptime->data.F32[i] + TO);
+            cntErr->data.F32[i] = AK*sqrt(FL*(exptime->data.F32[i] + TK)) / (exptime->data.F32[i] + TO);
+        }
+
+        pmShutterCorrection*guess = pmShutterCorrectionGuess (exptime, counts);
+        skip_start(guess == NULL, 0, "Skipping tests because pmShutterCorrectionGuess() failed");
+        pmShutterCorrection *pars = pmShutterCorrectionFullFit (exptime, counts, cntErr, guess);
+
+        // with fine log-spaced times well-sampling TO and TK,
+        // we can expect accurate guesses
+        ok(pars != NULL, "pmShutterCorrection successfully allocated by FullFit");
+        skip_start(pars == NULL, 0, "Skipping tests because pmShutterCorrectionAlloc() failed");
+        ok(fabs(pars->scale  - AK) < 0.01, "scale fit is close enough (got %f vs %f)",  pars->scale, AK);
+        ok(fabs(pars->offset - TK) < 0.01, "offset fit is close enough (got %f vs %f)", pars->offset, TK);
+        ok(fabs(pars->offref - TO) < 0.01, "offref fit is close enough (got %f vs %f)", pars->offref, TO);
+        skip_end();
+
+        psFree(pars);
+        skip_end();
+
+        psFree(guess);
+        psFree(exptime);
+        psFree(counts);
+        psFree(cntErr);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // XXX should add tests with the input counts scattered with GaussDev...
+
+    // test linear fitting
+    diag("pmShutterCorrectionLinFit tests : linear-spaced exptimes");
+    {
+        psMemId id = psMemGetId();
+
+        int NPTS = 20;
+        float FL = 10000.0;
+        float AK = 5.0;
+        float TK = 0.2;
+        float TO = 0.1;
+        psVector *exptime = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *counts  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *cntErr  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        exptime->n = counts->n = cntErr->n = NPTS;
+
+        for (int i = 0; i < exptime->n; i++) {
+            exptime->data.F32[i] = i*0.1;
+            counts->data.F32[i] = AK*(exptime->data.F32[i] + TK) / (exptime->data.F32[i] + TO);
+            cntErr->data.F32[i] = AK*sqrt(FL*(exptime->data.F32[i] + TK)) / (exptime->data.F32[i] + TO);
+        }
+
+        pmShutterCorrection*guess = pmShutterCorrectionGuess (exptime, counts);
+        skip_start(guess == NULL, 0, "Skipping tests because pmShutterCorrectionGuess() failed");
+        pmShutterCorrection *full = pmShutterCorrectionFullFit (exptime, counts, cntErr, guess);
+        pmShutterCorrection *pars = pmShutterCorrectionLinFit (exptime, counts, cntErr, NULL, full->offref, 1, 5, 0);
+
+        // with fine log-spaced times well-sampling TO and TK,
+        // we can expect accurate guesses
+        ok(pars != NULL, "pmShutterCorrection successfully allocated by FullFit");
+        skip_start(pars == NULL, 0, "Skipping tests because pmShutterCorrectionAlloc() failed");
+        ok(fabs(pars->scale  - AK) < 0.01, "scale fit is close enough (got %f vs %f)",  pars->scale, AK);
+        ok(fabs(pars->offset - TK) < 0.01, "offset fit is close enough (got %f vs %f)", pars->offset, TK);
+        ok(fabs(pars->offref - TO) < 0.01, "offref fit is close enough (got %f vs %f)", pars->offref, TO);
+        skip_end();
+
+        psFree(pars);
+        psFree(full);
+        skip_end();
+
+        psFree(guess);
+        psFree(exptime);
+        psFree(counts);
+        psFree(cntErr);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // test linear fitting
+    diag("pmShutterCorrectionLinFit tests : log-spaced exptimes");
+    {
+        psMemId id = psMemGetId();
+
+        int NPTS = 40;
+        float FL = 10000.0;
+        float AK = 1.0;
+        float TK = 0.2;
+        float TO = 0.1;
+        psVector *exptime = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *counts  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        psVector *cntErr  = psVectorAlloc (NPTS, PS_TYPE_F32);
+        exptime->n = counts->n = cntErr->n = NPTS;
+
+        for (int i = 0; i < exptime->n; i++) {
+            exptime->data.F32[i] = pow(10.0, -2 + i*0.1);
+            counts->data.F32[i] = AK*(exptime->data.F32[i] + TK) / (exptime->data.F32[i] + TO);
+            cntErr->data.F32[i] = AK*sqrt(FL*(exptime->data.F32[i] + TK)) / (exptime->data.F32[i] + TO);
+        }
+
+        pmShutterCorrection *guess = pmShutterCorrectionGuess (exptime, counts);
+        skip_start(guess == NULL, 0, "Skipping tests because pmShutterCorrectionGuess() failed");
+        pmShutterCorrection *full = pmShutterCorrectionFullFit (exptime, counts, cntErr, guess);
+        pmShutterCorrection *pars = pmShutterCorrectionLinFit (exptime, counts, cntErr, NULL, full->offref, 1, 5, 0);
+
+        // with fine log-spaced times well-sampling TO and TK,
+        // we can expect accurate guesses
+        ok(pars != NULL, "pmShutterCorrection successfully allocated by FullFit");
+        skip_start(pars == NULL, 0, "Skipping tests because pmShutterCorrectionAlloc() failed");
+        ok(fabs(pars->scale  - AK) < 0.01, "scale fit is close enough (got %f vs %f)",  pars->scale, AK);
+        ok(fabs(pars->offset - TK) < 0.01, "offset fit is close enough (got %f vs %f)", pars->offset, TK);
+        ok(fabs(pars->offref - TO) < 0.01, "offref fit is close enough (got %f vs %f)", pars->offref, TO);
+        skip_end();
+
+        psFree(pars);
+        psFree(full);
+        skip_end();
+
+        psFree(guess);
+        psFree(exptime);
+        psFree(counts);
+        psFree(cntErr);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/detrend/tst_pmFlatField.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/tst_pmFlatField.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/tst_pmFlatField.c	(revision 15358)
@@ -0,0 +1,290 @@
+/** @file tst_pmFlatField.c
+ *
+ *  @brief Contains the tests for pmFlatField.c:
+ *
+ *    Test A - Divide input image by flat image
+ *    Test B - Mask flat image data
+ *    Test C - Mask flat image data starting with non-null mask
+ *    Test E - Attempt to use null input image
+ *    Test F - Attempt tp use null flat image
+ *    Test G - Attempt to use input image bigger than flat image
+ *    Test H - Attempt to use input image mask bigger than flat image
+ *    Test I - Attempt to use offset greater than input image
+ *    Test J - Attempt to use complex input image
+ *    Test K - Attempt to use complex flat image
+ *    Test L - Attempt to use non-equal input and flat image types
+ *    Test M - Attempt to use non-mask type mask image
+ *
+ * XXX: Added a mask argument to pmFlatField().  Must add tests.  For now, all
+ * masks are NULL.
+ *
+ *  @author Ross Harman, MHPCC
+ *
+ *  XXX: I added the CELL.TRIMSEC region code but there are not tests for it.
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-11-15 20:09:03 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+
+#include "psTest.h"
+#include "pslib.h"
+#include "pmFlatField.h"
+
+
+#define PRINT_MATRIX(IMAGE,TYPE,STRING)                                                                      \
+printf(STRING);                                                                                              \
+printf("\n");                                                                                                \
+for(int i=IMAGE->numRows-1; i>-1; i--) {                                                                     \
+    for(int j=0; j<IMAGE->numCols; j++) {                                                                    \
+        if(PS_IS_PSELEMTYPE_COMPLEX(IMAGE->type.type)) {                                                     \
+            printf("%f+%fi ", creal(IMAGE->data.TYPE[i][j]), cimag(IMAGE->data.TYPE[i][j]));                 \
+        } else if(PS_IS_PSELEMTYPE_INT(IMAGE->type.type)) {                                                  \
+            printf("%d ", (int)IMAGE->data.TYPE[i][j]);                                                      \
+        } else {                                                                                             \
+            printf("%f ", (double)IMAGE->data.TYPE[i][j]);                                                   \
+        }                                                                                                    \
+    }                                                                                                        \
+    printf("\n");                                                                                            \
+}                                                                                                            \
+printf("\n");
+
+
+#define CREATE_AND_SET_IMAGE(NAME,TYPE,VALUE,NROWS,NCOLS)                                                    \
+psImage *NAME = (psImage*)psImageAlloc(NCOLS,NROWS,PS_TYPE_##TYPE);                                          \
+for(int i=0; i<NAME->numRows; i++) {                                                                         \
+    for(int j=0; j<NAME->numCols; j++) {                                                                     \
+        NAME->data.TYPE[i][j] = VALUE;                                                                       \
+    }                                                                                                        \
+}
+
+
+static int testFlatField(void);
+
+
+testDescription tests[] = {
+                              {testFlatField, 753, "pmFlatField", 0, false},
+                              {NULL}
+                          };
+
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    return !runTestSuite(stderr, "Test Point Driver", tests, argc, argv);
+}
+
+
+int testFlatField( void )
+{
+    // Test A - Divide input image by flat image
+    printPositiveTestHeader(stdout, "pmFlatField", "Test A - Divide input image by flat image");
+    CREATE_AND_SET_IMAGE(inImage,F64,6.0,3,3)
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    inReadout->image = inImage;
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+    CREATE_AND_SET_IMAGE(inMask, U8, 0, 3,3);
+    inReadout->mask = inMask;
+    PRINT_MATRIX((inReadout->mask),U8,"Input mask:");
+    PRINT_MATRIX(inImage,F64,"Input image:");
+
+    CREATE_AND_SET_IMAGE(flatImage1,F64,2.0,3,3)
+    pmReadout *flatReadout = pmReadoutAlloc(NULL);
+    flatReadout->row0 = 0;
+    flatReadout->col0 = 0;
+    flatReadout->image = flatImage1;
+    PRINT_MATRIX(flatImage1,F64,"Flat image:");
+
+    if ( !pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test A - Returned false should be true");
+        return 1;
+    }
+    PRINT_MATRIX(inImage,F64, "Resulting image:");
+    printFooter(stdout, "pmFlatField", "Test A - Divide input image by flat image", true);
+    printf("\n\n\n");
+
+
+    // Test B - Mask flat image data
+    printPositiveTestHeader(stdout, "pmFlatField", "Test B - Mask flat image data");
+    PRINT_MATRIX(inImage, F64, "Input image:");
+    CREATE_AND_SET_IMAGE(flatImage2,F64,0.0,3,3)
+    PRINT_MATRIX(flatImage2, F64, "Flat image:");
+    flatReadout->image = flatImage2;
+    if ( !pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test B - Returned false should be true");
+        return 2;
+    }
+    PRINT_MATRIX(inReadout->mask, PS_TYPE_MASK_DATA, "Resulting mask:");
+    PRINT_MATRIX(inImage,F64,"Resulting image:");
+    printFooter(stdout, "pmFlatField", "Test B - Mask flat image data", true);
+    printf("\n\n\n");
+
+
+    // Test C - Mask flat image data starting with non-null mask
+    printPositiveTestHeader(stdout, "pmFlatField", "Test C - Mask flat image data starting with non-null mask");
+    PRINT_MATRIX(inImage, F64, "Input image:");
+    flatImage2->data.F64[0][0] = 3.0;
+    flatImage2->data.F64[0][1] = -3.0;
+    PRINT_MATRIX(flatImage2, F64, "Flat image:");
+    CREATE_AND_SET_IMAGE(mask1,U8,0,3,3);
+    psFree(inReadout->mask);
+    inReadout->mask = mask1;
+    if ( !pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test C - Returned false should be true");
+        return 3;
+    }
+    PRINT_MATRIX(flatImage2, F64, "Flat image out:");
+    PRINT_MATRIX(inReadout->mask, PS_TYPE_MASK_DATA,"Resulting mask:");
+    PRINT_MATRIX(inImage,F64,"Resulting image:");
+    printFooter(stdout, "pmFlatField", "Test C - Mask flat image data starting with non-null mask", true);
+    printf("\n\n\n");
+
+
+    // Test D - Attempt to use null flat readout
+    printNegativeTestHeader(stdout,"pmFlatField", "Test D - Attempt to use null flat readout",
+                            "Null not allowed for flat readout", 0);
+    if( pmFlatField(inReadout, NULL) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test D - Returned true should be false");
+        return 4;
+    }
+    printFooter(stdout, "pmFlatField", "Test D - Attempt to use null flat readout", true);
+    printf("\n\n\n");
+
+
+    // Test E - Attempt to use null input image
+    printNegativeTestHeader(stdout,"pmFlatField", "Test E - Attempt to use null input image",
+                            "Null not allowed for input image", 0);
+    psImage *temp = inReadout->image;
+    inReadout->image = NULL;
+    if ( pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test E - Returned true should be false" );
+        return 5;
+    }
+    inReadout->image = temp    ;
+    printFooter(stdout, "pmFlatField", "Test E - Attempt to use null input image", true);
+    printf("\n\n\n");
+
+
+    // Test F - Attempt tp use null flat image
+    printNegativeTestHeader(stdout,"pmFlatField", "Test F - Attempt tp use null flat image",
+                            "Null not allowed for flat image", 0);
+    temp = flatReadout->image;
+    flatReadout->image = NULL;
+    if ( pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test F - Returned true should be false" );
+        return 6;
+    }
+    flatReadout->image = temp;
+    printFooter(stdout, "pmFlatField", "Test F - Attempt tp use null flat image", true);
+    printf("\n\n\n");
+
+
+    // Test G - Attempt to use input image bigger than flat image
+    printNegativeTestHeader(stdout,"pmFlatField", "Test G - Attempt to use input image bigger than flat image",
+                            "Input image size exceeds that of flat image", 0);
+    CREATE_AND_SET_IMAGE(smallFlat,F64,0.0,2,2);
+    temp = flatReadout->image;
+    flatReadout->image = smallFlat;
+    if ( pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test G - Returned true should be false");
+        return 7;
+    }
+    flatReadout->image = temp;
+    printFooter(stdout, "pmFlatField", "Test G - Attempt to use input image bigger than flat image", true);
+    printf("\n\n\n");
+
+    // Test H - Attempt to use input image mask bigger than flat image
+    printNegativeTestHeader(stdout,"pmFlatField", "Test H - Attempt to use input image mask bigger than flat image",
+                            "Input image mask size exceeds that of flat image", 0);
+    CREATE_AND_SET_IMAGE(largeMask,F64,0.0,5,5);
+    inReadout->mask = largeMask;
+    if ( pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test H - Returned true should be false");
+        return 8;
+    }
+    printFooter(stdout, "pmFlatField", "Test H - Attempt to use input image mask bigger than flat image", true);
+    printf("\n\n\n");
+    inReadout->mask = mask1;
+
+    // Test I - Attempt to use offset greater than input image
+    printNegativeTestHeader(stdout,"pmFlatField", "Test I - Attempt to use offset greater than input image",
+                            "Total offset >= input image size", 0);
+    *(int*)&inReadout->col0 = 50;
+    *(int*)&inReadout->row0 = 50;
+    if ( pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test I - Returned true should be false");
+        return 9;
+    }
+    *(int*)&inReadout->col0 = 0;
+    *(int*)&inReadout->row0 = 0;
+    printFooter(stdout, "pmFlatField", "Test I - Attempt to use offset greater than input image", true);
+    printf("\n\n\n");
+
+
+    // Test J - Attempt to use complex input image
+    printNegativeTestHeader(stdout,"pmFlatField", "Test J - Attempt to use complex input image",
+                            "Complex types not allowed for input image", 0);
+    *(psElemType* ) & inReadout->image->type.type = PS_TYPE_C64;
+    if ( pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test J - Returned true should be false");
+        return 10;
+    }
+    *(psElemType* ) & inReadout->image->type.type = PS_TYPE_F64;
+    printFooter(stdout, "pmFlatField", "Test J - Attempt to use complex input image", true);
+    printf("\n\n\n");
+
+
+    // Test K - Attempt to use complex flat image
+    printNegativeTestHeader(stdout,"pmFlatField", "Test K - Attempt to use complex flat image",
+                            "Complex types not allowed for flat image", 0);
+    *(psElemType* ) & flatReadout->image->type.type = PS_TYPE_C64;
+    if ( pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test K - Returned ture should be false");
+        return 11;
+    }
+    *(psElemType* ) & flatReadout->image->type.type = PS_TYPE_F64;
+    printFooter(stdout, "pmFlatField", "Test K - Attempt to use complex flat image", true);
+    printf("\n\n\n");
+
+
+    // Test L - Attempt to use non-equal input and flat image types
+    printNegativeTestHeader(stdout,"pmFlatField", "Test L - Attempt to use non-equal input and flat image types",
+                            "Input and flat image types differ", 0);
+    *(psElemType* ) & flatReadout->image->type.type = PS_TYPE_F32;
+    if ( pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test L - Returned true should be false");
+        return 12;
+    }
+    *(psElemType* ) & flatReadout->image->type.type = PS_TYPE_F64;
+    printFooter(stdout, "pmFlatField", "Test L - Attempt to use non-equal input and flat image types", true);
+    printf("\n\n\n");
+
+
+    // Test M - Attempt to use non-mask type mask image
+    printNegativeTestHeader(stdout,"pmFlatField", "Test M - Attempt to use non-mask type mask image",
+                            "Mask must be PS_TYPE_MASK type", 0);
+    *(psElemType* ) & inReadout->mask->type.type = PS_TYPE_F32;
+    if ( pmFlatField(inReadout, flatReadout) ) {
+        psError(PS_ERR_UNKNOWN,true,"Test M - Returned true should be false");
+        return 13;
+    }
+    *(psElemType* ) & inReadout->mask->type.type = PS_TYPE_MASK;
+    printFooter(stdout, "pmFlatField", "Test M - Attempt to use non-mask type mask image", true);
+    printf("\n\n\n");
+
+
+    // Free memory
+    psFree(inReadout);
+    psFree(flatReadout);
+    //psFree(inImage);
+    psFree(flatImage1);
+    //psFree(flatImage1);
+    psFree(smallFlat);
+    psFree(largeMask);
+
+    return 0;
+}
+
Index: /branches/eam_branch_20071023/psModules/test/detrend/tst_pmMaskBadPixels.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/tst_pmMaskBadPixels.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/tst_pmMaskBadPixels.c	(revision 15358)
@@ -0,0 +1,381 @@
+/** @file tst_pmMaskBadPixels.c
+ *
+ *  @brief Contains the tests for pmMaskBadPixels:
+ *
+ *    Test A - Create mask based on maskVal argument
+ *    Test B - Create mask based on saturation argument
+ *    Test C - Create mask based on growVal and grow arguments
+ *    Test D - Auto Create mask based on maskVal argument
+ *    Test E - Attempt to use null mask
+ *    Test F - Attempt tp use null input image
+ *    Test G - Attempt to use input image bigger than mask
+ *    Test H - Attempt to use input image mask bigger than mask
+ *    Test I - Attempt to use offset greater than input image
+ *    Test J - Attempt to use complex input image
+ *    Test K - Attempt to use non-mask type mask image
+ *
+ *  @author Ross Harman, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-11-15 20:09:03 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+
+#include "psTest.h"
+#include "pslib.h"
+#include "pmMaskBadPixels.h"
+
+
+#define PRINT_MATRIX(IMAGE,TYPE,STRING)                                                                      \
+printf(STRING);                                                                                              \
+printf("\n");                                                                                                \
+for(int i=(IMAGE)->numRows-1; i>-1; i--) {                                                                     \
+    for(int j=0; j<(IMAGE)->numCols; j++) {                                                                    \
+        if(PS_IS_PSELEMTYPE_COMPLEX((IMAGE)->type.type)) {                                                     \
+            printf("%f+%fi ", creal((IMAGE)->data.TYPE[i][j]), cimag((IMAGE)->data.TYPE[i][j]));                 \
+        } else if(PS_IS_PSELEMTYPE_INT((IMAGE)->type.type)) {                                                  \
+            printf("%d", (int)(IMAGE)->data.TYPE[i][j]);                                                       \
+        } else {                                                                                             \
+            printf("%f", (double)(IMAGE)->data.TYPE[i][j]);                                                    \
+        }                                                                                                    \
+    }                                                                                                        \
+    printf("\n");                                                                                            \
+}                                                                                                            \
+printf("\n");
+
+
+#define CREATE_AND_SET_IMAGE(NAME,TYPE,VALUE,NROWS,NCOLS)                                                    \
+(NAME) = (psImage*)psImageAlloc(NCOLS,NROWS,PS_TYPE_##TYPE);                                             \
+for(int i=0; i<(NAME)->numRows; i++) {                                                                         \
+    for(int j=0; j<(NAME)->numCols; j++) {                                                                     \
+        (NAME)->data.TYPE[i][j] = VALUE;                                                                       \
+    }                                                                                                        \
+}
+
+static int testMaskBadPixels1(void);
+static int testMaskBadPixels2(void);
+static int testMaskBadPixels3(void);
+static int testMaskBadPixels4(void);
+static int testMaskBadPixels5(void);
+static int testMaskBadPixels6(void);
+static int testMaskBadPixels7(void);
+static int testMaskBadPixels8(void);
+static int testMaskBadPixels9(void);
+static int testMaskBadPixels10(void);
+static int testMaskBadPixels11(void);
+
+
+testDescription tests[] = {
+                              {testMaskBadPixels1, 885, "pmMaskBadPixels - Create mask based on maskVal argument", 0, false},
+                              {testMaskBadPixels2, 885, "pmMaskBadPixels - Create mask based on saturation argument", 0, false},
+                              {testMaskBadPixels3, 885, "pmMaskBadPixels - Create mask based on growVal and grow arguments", 0, false},
+                              {testMaskBadPixels4, 885, "pmMaskBadPixels - Auto create mask based on maskVal argument", 0, false},
+                              {testMaskBadPixels5, 885, "pmMaskBadPixels - Attempt to use null mask", 0, false},
+                              {testMaskBadPixels6, 885, "pmMaskBadPixels - Attempt tp use null input image", 0, false},
+                              {testMaskBadPixels7, 885, "pmMaskBadPixels - Attempt to use input image bigger than mask", 0, false},
+                              {testMaskBadPixels8, 885, "pmMaskBadPixels - Attempt to use input image mask bigger than mask", 0, false},
+                              {testMaskBadPixels9, 885, "pmMaskBadPixels - Attempt to use offset greater than input image", 0, false},
+                              {testMaskBadPixels10, 885, "pmMaskBadPixels - Attempt to use complex input image", 0, false},
+                              {testMaskBadPixels11, 885, "pmMaskBadPixels - Attempt to use non-mask type mask image", 0, false        },
+                              {NULL}
+                          };
+
+/*
+    #define PS_TYPE_MASK PS_TYPE_U8
+    #define PS_TYPE_MASK_DATA U8
+    #define PS_TYPE_MASK_NAME "psU8"
+*/
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    return !runTestSuite(stderr, "Test Point Driver", tests, argc, argv);
+}
+
+
+#define NUM_ROWS 50
+#define NUM_COLS 50
+#define DEFAULT_IMAGE_VAL 0.0
+#define DEFAULT_MASK_VAL 0
+#define MASK_VAL 1
+#define SAT_VAL  100.0
+#define GROW_VAL 1
+#define GROW_RAD 10
+int testMaskBadPixels1( void )
+{
+    //
+    // Test A - Create mask based on maskVal argument
+    //
+
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(inReadout->image, F32, DEFAULT_IMAGE_VAL, NUM_ROWS, NUM_COLS);
+    inReadout->image->row0 = 0;
+    inReadout->image->col0 = 0;
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+
+    pmReadout *maskReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(maskReadout->image, U8, DEFAULT_MASK_VAL, NUM_ROWS, NUM_COLS)
+    maskReadout->image->data.PS_TYPE_MASK_DATA[NUM_ROWS/2][NUM_COLS/2]=1;
+    maskReadout->image->row0 = 0;
+    maskReadout->image->col0 = 0;
+
+    PRINT_MATRIX(maskReadout->image, U8, "Data mask:");
+
+    pmMaskBadPixels(inReadout, maskReadout, MASK_VAL, SAT_VAL, GROW_VAL, GROW_RAD);
+    PRINT_MATRIX(inReadout->mask, PS_TYPE_MASK_DATA, "Resulting mask:");
+
+    psFree(inReadout);
+    psFree(maskReadout);
+
+    return 0;
+}
+
+int testMaskBadPixels2( void )
+{
+    //
+    // Test B - Create mask based on saturation argument
+    //
+
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(inReadout->image, F32, DEFAULT_IMAGE_VAL, NUM_ROWS, NUM_COLS);
+    inReadout->image->row0 = 0;
+    inReadout->image->col0 = 0;
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+    inReadout->image->data.F32[NUM_ROWS/2][NUM_COLS/2] = 150.0;
+
+    pmReadout *maskReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(maskReadout->image, U8, DEFAULT_MASK_VAL, NUM_ROWS, NUM_COLS)
+
+    //PS_IMAGE_PRINT_F32(inReadout->image);
+    PRINT_MATRIX(maskReadout->image, U8, "Data mask:");
+    pmMaskBadPixels(inReadout, maskReadout, MASK_VAL, SAT_VAL, GROW_VAL, GROW_RAD);
+    PRINT_MATRIX(inReadout->mask, U8, "Resulting mask:");
+
+    psFree(inReadout);
+    psFree(maskReadout);
+
+    return 0;
+}
+
+int testMaskBadPixels3( void )
+{
+    //
+    // Test C - Create mask based on growVal and grow arguments
+    //
+
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(inReadout->image, F32, DEFAULT_IMAGE_VAL, NUM_ROWS, NUM_COLS);
+    inReadout->image->row0 = 0;
+    inReadout->image->col0 = 0;
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+    inReadout->image->data.F32[NUM_ROWS/2][NUM_COLS/2]=GROW_VAL;
+    inReadout->image->data.F32[NUM_ROWS/4][NUM_COLS/4]=GROW_VAL;
+    inReadout->image->data.F32[NUM_ROWS/4][NUM_COLS-(NUM_COLS/4)]=GROW_VAL;
+
+    pmReadout *maskReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(maskReadout->image, U8, DEFAULT_MASK_VAL, NUM_ROWS, NUM_COLS);
+
+    PRINT_MATRIX(maskReadout->image, U8, "Data mask:");
+    //PS_IMAGE_PRINT_F32(inReadout->image);
+    pmMaskBadPixels(inReadout, maskReadout, MASK_VAL, SAT_VAL, GROW_VAL, GROW_RAD);
+    PRINT_MATRIX(inReadout->mask, U8, "Resulting mask:");
+
+    psFree(inReadout);
+    psFree(maskReadout);
+
+    return 0;
+}
+
+int testMaskBadPixels4( void )
+{
+    //
+    // Test D - Auto Create mask based on maskVal argument
+    //
+
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(inReadout->image, F32, DEFAULT_IMAGE_VAL, NUM_ROWS, NUM_COLS);
+    inReadout->image->row0 = 0;
+    inReadout->image->col0 = 0;
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+
+    pmReadout *maskReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(maskReadout->image, U8, DEFAULT_MASK_VAL, NUM_ROWS, NUM_COLS)
+    maskReadout->image->data.PS_TYPE_MASK_DATA[NUM_ROWS/2][NUM_COLS/2]=1;
+
+    PRINT_MATRIX(maskReadout->image, U8, "Data mask:");
+    pmMaskBadPixels(inReadout, maskReadout, MASK_VAL, SAT_VAL, GROW_VAL, GROW_RAD);
+    PRINT_MATRIX(inReadout->mask, U8, "Resulting mask:");
+
+    psFree(inReadout);
+    psFree(maskReadout);
+
+    return 0;
+}
+
+int testMaskBadPixels5( void )
+{
+    //
+    // Test E - Attempt to use null mask
+    //
+
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(inReadout->image, F32, DEFAULT_IMAGE_VAL, NUM_ROWS, NUM_COLS);
+    inReadout->image->row0 = 0;
+    inReadout->image->col0 = 0;
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+
+    pmMaskBadPixels(inReadout, NULL, MASK_VAL, SAT_VAL, GROW_VAL, GROW_RAD);
+    psFree(inReadout);
+
+    return 0;
+}
+
+int testMaskBadPixels6( void )
+{
+    //
+    // Test F - Attempt tp use null input image
+    //
+
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+
+    pmReadout *maskReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(maskReadout->image, U8, DEFAULT_MASK_VAL, NUM_ROWS, NUM_COLS)
+
+    pmMaskBadPixels(inReadout, maskReadout, MASK_VAL, SAT_VAL, GROW_VAL, GROW_RAD);
+
+    psFree(inReadout);
+    psFree(maskReadout);
+
+    return 0;
+}
+
+int testMaskBadPixels7( void )
+{
+    //
+    // Test G - Attempt to use input image bigger than mask
+    //
+
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(inReadout->image, F32, DEFAULT_IMAGE_VAL, NUM_ROWS+10, NUM_COLS+10);
+    inReadout->image->row0 = 0;
+    inReadout->image->col0 = 0;
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+
+    pmReadout *maskReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(maskReadout->image, U8, DEFAULT_MASK_VAL, NUM_ROWS, NUM_COLS)
+
+    pmMaskBadPixels(inReadout, maskReadout, MASK_VAL, SAT_VAL, GROW_VAL, GROW_RAD);
+
+    psFree(inReadout);
+    psFree(maskReadout);
+
+    return 0;
+}
+
+int testMaskBadPixels8( void )
+{
+    //
+    // Test H - Attempt to use mask bigger than image
+    //
+
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(inReadout->image, F32, DEFAULT_IMAGE_VAL, NUM_ROWS, NUM_COLS);
+    inReadout->image->row0 = 0;
+    inReadout->image->col0 = 0;
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+
+    pmReadout *maskReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(maskReadout->image, U8, DEFAULT_MASK_VAL, NUM_ROWS+10, NUM_COLS+10)
+
+    pmMaskBadPixels(inReadout, maskReadout, MASK_VAL, SAT_VAL, GROW_VAL, GROW_RAD);
+
+    psFree(inReadout);
+    psFree(maskReadout);
+
+    return 0;
+}
+
+int testMaskBadPixels9( void )
+{
+    //
+    // Test I - Attempt to use offset greater than input image
+    //
+
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(inReadout->image, F32, DEFAULT_IMAGE_VAL, NUM_ROWS, NUM_COLS);
+    inReadout->image->row0 = 0;
+    inReadout->image->col0 = 0;
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+    *(int*)&inReadout->image->col0 = 150;
+    *(int*)&inReadout->image->row0 = 150;
+
+    pmReadout *maskReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(maskReadout->image, U8, DEFAULT_MASK_VAL, NUM_ROWS, NUM_COLS)
+    pmMaskBadPixels(inReadout, maskReadout, MASK_VAL, SAT_VAL, GROW_VAL, GROW_RAD);
+
+    psFree(inReadout);
+    psFree(maskReadout);
+
+    return 0;
+}
+
+int testMaskBadPixels10( void )
+{
+    //
+    // Test J - Attempt to use complex input image
+    //
+
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(inReadout->image, C64, DEFAULT_IMAGE_VAL, NUM_ROWS, NUM_COLS);
+    inReadout->image->row0 = 0;
+    inReadout->image->col0 = 0;
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+
+    pmReadout *maskReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(maskReadout->image, U8, DEFAULT_MASK_VAL, NUM_ROWS, NUM_COLS)
+
+    pmMaskBadPixels(inReadout, maskReadout, MASK_VAL, SAT_VAL, GROW_VAL, GROW_RAD);
+
+    psFree(inReadout);
+    psFree(maskReadout);
+
+    return 0;
+}
+
+int testMaskBadPixels11( void )
+{
+    //
+    // Test K - Attempt to use mask image with wrong data type.
+    //
+
+    pmReadout *inReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(inReadout->image, F32, DEFAULT_IMAGE_VAL, NUM_ROWS, NUM_COLS);
+    inReadout->image->row0 = 0;
+    inReadout->image->col0 = 0;
+    inReadout->row0 = 0;
+    inReadout->col0 = 0;
+
+    pmReadout *maskReadout = pmReadoutAlloc(NULL);
+    CREATE_AND_SET_IMAGE(maskReadout->image, F64, DEFAULT_MASK_VAL, NUM_ROWS, NUM_COLS)
+
+    pmMaskBadPixels(inReadout, maskReadout, MASK_VAL, SAT_VAL, GROW_VAL, GROW_RAD);
+
+    psFree(inReadout);
+    psFree(maskReadout);
+
+    return 0;
+}
+
Index: /branches/eam_branch_20071023/psModules/test/detrend/tst_pmNonLinear.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/tst_pmNonLinear.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/tst_pmNonLinear.c	(revision 15358)
@@ -0,0 +1,268 @@
+/* @file tst_pmNonLinear.c
+ *
+ *  @brief Contains the tests for pmNonLinear.c:
+ *
+ * test00: This code will create a simple polynomial, and call
+ * pmNonLinearityPolynomial() for a variety of image sizes [(1, 1), (1,
+ * N), (N, 1), (N, N)].  
+ *
+ * test01: This code will create simple table lookup vectors, and call
+ * pmNonLinearityPolynomial() for a variety of image sizes [(1, 1), (1,
+ * N), (N, 1), (N, N)].  
+ *
+ * test02, test03: This code tests the functions with various unallowable
+ * input parameters (NULLS) and incorrect vector sizes.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  XXX: Add tests in which the lookup file has incorrect number of entries,
+ *  and where the data is outside the pmReadout range.
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-01-26 21:10:51 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#include "psTest.h"
+#include "pslib.h"
+#include "pmNonLinear.h"
+static int test00(void);
+static int test01(void);
+static int test02(void);
+static int test03(void);
+testDescription tests[] = {
+                              {test00, 000, "pmNonLinearityPolynomial", true, false},
+                              {test01, 000, "pmNonLinearityLookup", true, false},
+                              {test02, 000, "pmNonLinearityPolynomial(): error/warning conditions", true, false},
+                              {test03, 000, "pmNonLinearityLookup(): error/warning conditions", true, false},
+                              {NULL}
+                          };
+
+#define NUM_ROWS 8
+#define NUM_COLS 8
+#define LOOKUP_FILENAME ".tmp_tst_pmNonLinearLookupFile"
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    //
+    // We generate a lookup file for future tests.  We should probably remove
+    // it when we're done.
+    //
+    FILE *fp = fopen(LOOKUP_FILENAME, "w");
+    ;
+    for (psS32 i=0;i<PS_MAX(NUM_COLS, NUM_ROWS)*3;i++) {
+        fprintf(fp, "%f %f\n", (float) i, (float) (2 * i));
+    }
+    fclose(fp);
+
+    //    system("rm LOOKUP_FILENAME");
+    return !runTestSuite(stderr, "Test Point Driver", tests, argc, argv);
+}
+
+int doNonLinearityPolynomialTest(int numCols, int numRows)
+{
+    int i;
+    int j;
+    float actual;
+    float expect;
+    int testStatus = true;
+    psImage *myImage = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    pmReadout *myReadout = pmReadoutAlloc(NULL);
+    myReadout->image = myImage;
+    psPolynomial1D *myPoly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 2);
+    myPoly->coeff[1] = 1.0;
+
+    printPositiveTestHeader(stdout, "pmNonLinear", "doNonLinearityPolynomialTest");
+
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            myReadout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+
+    myReadout = pmNonLinearityPolynomial(myReadout, myPoly);
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            expect = psPolynomial1DEval(myPoly, (float) (i + j));
+            actual = myReadout->image->data.F32[i][j];
+            if (FLT_EPSILON < fabs(expect - actual)) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                testStatus = false;
+            }
+        }
+    }
+
+
+    psFree(myReadout);
+    psFree(myPoly);
+    printFooter(stdout, "pmNonLinear", "doNonLinearityPolynomialTest", true);
+    return(testStatus);
+}
+
+int test00( void )
+{
+    int testStatus = 0;
+
+    testStatus |= doNonLinearityPolynomialTest(1, 1);
+    testStatus |= doNonLinearityPolynomialTest(NUM_COLS, 1);
+    testStatus |= doNonLinearityPolynomialTest(1, NUM_ROWS);
+    testStatus |= doNonLinearityPolynomialTest(NUM_COLS, NUM_ROWS);
+
+    return(testStatus);
+}
+
+int doNonLinearityLookupTest(int numCols, int numRows)
+{
+    int i;
+    int j;
+    float actual;
+    float expect;
+    int testStatus = true;
+    psImage *myImage = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    pmReadout *myReadout = pmReadoutAlloc(NULL);
+    myReadout->image = myImage;
+
+    printPositiveTestHeader(stdout, "pmNonLinear", "doNonLinearityLookupTest");
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            myReadout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+
+    myReadout = pmNonLinearityLookup(myReadout, LOOKUP_FILENAME);
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            expect = (float) (2 * (i + j));
+            actual = myReadout->image->data.F32[i][j];
+            if (FLT_EPSILON < fabs(expect - actual)) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                testStatus = false;
+            }
+        }
+    }
+
+    psFree(myReadout);
+    printFooter(stdout, "pmNonLinear", "doNonLinearityLookupTest", true);
+    return(testStatus);
+}
+
+int test01( void )
+{
+    int testStatus = 0;
+
+    testStatus |= doNonLinearityLookupTest(1, 1);
+    testStatus |= doNonLinearityLookupTest(NUM_COLS, 1);
+    testStatus |= doNonLinearityLookupTest(1, NUM_ROWS);
+    testStatus |= doNonLinearityLookupTest (NUM_COLS, NUM_ROWS);
+
+    return(testStatus);
+}
+
+int test02( void )
+{
+    int i;
+    int j;
+    int testStatus = true;
+    psImage *myImage = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+    pmReadout *myReadout = pmReadoutAlloc(NULL);
+    pmReadout *rc = NULL;
+    myReadout->image = myImage;
+    psPolynomial1D *myPoly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, 2);
+    myPoly->coeff[1] = 1.0;
+
+    printPositiveTestHeader(stdout, "pmNonLinear", "Testing bad input parameter conditions.");
+    for (i=0;i<NUM_ROWS;i++) {
+        for (j=0;j<NUM_COLS;j++) {
+            myReadout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+
+    printf("------------------------------------------------------------\n");
+    printf("Calling pmNonLinearityPolynomial() with NULL input readout.  Should generate error, return NULL.\n");
+    rc = pmNonLinearityPolynomial(NULL, myPoly);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmNonLinearityPolynomial() returned a non-NULL pmReadout\n");
+        testStatus = false;
+    }
+
+    printf("------------------------------------------------------------\n");
+    printf("Calling pmNonLinearityPolynomial() with NULL input readout->image.  Should generate error, return NULL.\n");
+    psImage *tmpImage = myReadout->image;
+    myReadout->image = NULL;
+    rc = pmNonLinearityPolynomial(myReadout, myPoly);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmNonLinearityPolynomial() returned a non-NULL pmReadout\n");
+        testStatus = false;
+    }
+    myReadout->image = tmpImage;
+
+    printf("------------------------------------------------------------\n");
+    printf("Calling pmNonLinearityPolynomial() with NULL polynomial.  Should generate error, return NULL.\n");
+    rc = pmNonLinearityPolynomial(myReadout, NULL);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmNonLinearityPolynomial() returned a non-NULL pmReadout\n");
+        testStatus = false;
+    }
+
+    psFree(myReadout);
+    psFree(myPoly);
+    return(testStatus);
+}
+
+
+int test03Init(pmReadout *myReadout)
+{
+    for (psS32 i=0;i<NUM_ROWS;i++) {
+        for (psS32 j=0;j<NUM_COLS;j++) {
+            myReadout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+    return(0);
+}
+
+int test03()
+{
+    int testStatus = true;
+    psImage *myImage = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+    pmReadout *myReadout = pmReadoutAlloc(NULL);
+    pmReadout *rc = NULL;
+    myReadout->image = myImage;
+
+    test03Init(myReadout);
+    printf("------------------------------------------------------------\n");
+    printf("Calling pmNonLinearityLookup() with NULL input pmReadout.  Should generate error, return NULL.\n");
+    rc = pmNonLinearityLookup(NULL, LOOKUP_FILENAME);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmNonLinearityPolynomial() returned a non-NULL pmReadout\n");
+        testStatus = false;
+    }
+
+    printf("------------------------------------------------------------\n");
+    printf("Calling pmNonLinearityLookup() with NULL input pmReadout->image.  Should generate error, return NULL.\n");
+    psImage *tmpImage = myReadout->image;
+    myReadout->image = NULL;
+    rc = pmNonLinearityLookup(myReadout, LOOKUP_FILENAME);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmNonLinearityPolynomial() returned a non-NULL pmReadout\n");
+        testStatus = false;
+    }
+    myReadout->image = tmpImage;
+
+    printf("------------------------------------------------------------\n");
+    printf("Calling pmNonLinearityLookup() with non-existent lookup file.\n");
+    rc = pmNonLinearityLookup(myReadout, "I_DONT_EXIST");
+    if (rc == NULL) {
+        printf("TEST ERROR: pmNonLinearityPolynomial() returned a NULL pmReadout\n");
+        testStatus = false;
+    }
+
+
+    printf("------------------------------------------------------------\n");
+    printf("Calling pmNonLinearityLookup() with one pixels outside inFlux range.  Should generate warnings.\n");
+
+    psFree(myReadout);
+
+    printFooter(stdout, "pmNonLinear", "Testing bad input parameter conditions.", true);
+    return(testStatus);
+}
Index: /branches/eam_branch_20071023/psModules/test/detrend/tst_pmSubtractBias.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/tst_pmSubtractBias.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/tst_pmSubtractBias.c	(revision 15358)
@@ -0,0 +1,1129 @@
+/** @file tst_pmSubtractBias.c
+ *
+ *  @brief Contains the tests for pmSubtractBias.c:
+ *
+ * test00a: This code will subtract full bias frames from the input image.
+ * XXX: Must test:
+ *  Various image offsets.
+ *  Various image size combinations.
+ *  Various data types for the bias and input images.
+ *  Ensure code works when CELL.TRIMSEC is not set.
+ * test00b: This code will subtract full dark frames from the input image.
+ * XXX: Must test:
+ *  Various image offsets.
+ *  Various image size combinations.
+ *  Various data types for the bias and input images.
+ *  Code properly determines CELL.DARKTIME from cell metadata.
+ *  Ensure code works when CELL.DARKTIME is not set.
+ *  Ensure code works when CELL.TRIMSEC is not set.
+ *  test03: Calculate a row overscan vector and subtract it from each
+ *  row in the input image.
+ * test05:
+ *
+ *  @author GLG, MHPCC
+ *
+ *  XXX: Memory leaks are not being detected.
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-05-25 22:02:23 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#include "psTest.h"
+#include "pslib.h"
+#include "pmSubtractBias.h"
+static int test00a(void);
+static int test00b(void);
+//static int test01(void);
+//static int test02(void);
+//static int test03(void);
+//static int test04(void);
+static int test05(void);
+testDescription tests[] = {
+                              {test00a, 000, "doSubtractBiasFullFrame", 0, true},
+                              {test00b, 000, "doSubtractDarkFullFrame", 0, true},
+                              //                              {test01, 000, "pmSubtractBias", 0, true},
+                              //                              {test02, 000, "pmSubtractBias", 0, true},
+                              //                              {test03, 000, "pmSubtractBias", 0, true},
+                              //                              {test04, 000, "pmSubtractBias", 0, true},
+                              {test05, 000, "pmSubtractBias", 0, false},
+                              {NULL}
+                          };
+
+psS32 currentId = 0;
+psS32 memLeaks = 0;             // XXX: remove
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+
+    psTraceSetLevel(".", 0);
+    psTraceSetLevel("spline1DFree", 0);
+    psTraceSetLevel("calculateSecondDerivs", 0);
+    psTraceSetLevel("vectorBinDisectF32", 0);
+    psTraceSetLevel("vectorBinDisectF64", 0);
+    psTraceSetLevel("p_psVectorBinDisect", 0);
+    psTraceSetLevel("psSpline1DAlloc", 0);
+    psTraceSetLevel("psVectorFitSpline1D", 0);
+    psTraceSetLevel("psSpline1DEval", 0);
+    psTraceSetLevel("psSpline1DEvalVector", 0);
+
+    psS32 currentId = psMemGetId(); // XXX: remove
+    psS32 memLeaks = 0;             // XXX: remove
+    if (0) {
+        PRINT_MEMLEAKS(0);
+    }
+
+    return !runTestSuite(stderr, "Test Point Driver", tests, argc, argv);
+}
+
+#define NUM_ROWS 8
+#define NUM_COLS 8
+#define MAX_HEADER_MSG_LENGTH 1000
+#define POLYNOMIAL_FIT_ORDER 2
+#define NUM_OVERSCANS 2
+/******************************************************************************
+doSubtractBiasFullFrame(): a sample pmReadout as well as a bias image are
+created and the bias image is subtracted from the pmReadout.
+ *****************************************************************************/
+int doSubtractBiasFullFrame(int numCols, int numRows)
+{
+    int i;
+    int j;
+    float actual;
+    float expect;
+    int testStatus = 0;
+    psImage *tmpImage1 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *tmpImage2 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    pmReadout *myReadout = pmReadoutAlloc(NULL);
+    pmReadout *myBias = pmReadoutAlloc(NULL);
+    myReadout->image = tmpImage1;
+    myBias->image = tmpImage2;
+
+    char *HeaderMessageStr = (char *) psAlloc(MAX_HEADER_MSG_LENGTH);
+    sprintf(HeaderMessageStr, "doSubtractBiasFullFrame(%d, %d)", numRows, numCols);
+    printPositiveTestHeader(stdout, "pmSubtractBias", HeaderMessageStr);
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            myReadout->image->data.F32[i][j] = (float) (i + j);
+            myBias->image->data.F32[i][j] = 1.0;
+        }
+    }
+
+    myReadout = pmSubtractBias(myReadout, NULL, PM_FIT_NONE, false,
+                               NULL, 0, myBias, NULL);
+
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            expect = ((float) (i + j)) - 1.0;
+            actual = myReadout->image->data.F32[i][j];
+            if (FLT_EPSILON < fabs(expect - actual)) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                testStatus = 1;
+            }
+        }
+    }
+
+
+    psFree(myReadout);
+    psFree(myBias);
+    printFooter(stdout, "pmSubtractBias", HeaderMessageStr, true);
+    psFree(HeaderMessageStr);
+    return(testStatus);
+}
+
+
+int test00a( void )
+{
+    int testStatus = 0;
+
+    testStatus |= doSubtractBiasFullFrame(1, 1);
+    testStatus |= doSubtractBiasFullFrame(NUM_COLS, 1);
+    testStatus |= doSubtractBiasFullFrame(1, NUM_ROWS);
+    testStatus |= doSubtractBiasFullFrame(NUM_COLS, NUM_ROWS);
+    return(testStatus);
+}
+
+
+/******************************************************************************
+doSubtractDarkFullFrame(): a sample pmReadout as well as a dark image are
+created and the dark image is subtracted from the pmReadout.
+ *****************************************************************************/
+int doSubtractDarkFullFrame(int numCols, int numRows)
+{
+    int i;
+    int j;
+    float actual;
+    float expect;
+    int testStatus = 0;
+    psImage *tmpImage1 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *tmpImage2 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    pmReadout *myReadout = pmReadoutAlloc(NULL);
+    pmReadout *myDark = pmReadoutAlloc(NULL);
+    myReadout->image = tmpImage1;
+    myDark->image = tmpImage2;
+
+    char *HeaderMessageStr = (char *) psAlloc(MAX_HEADER_MSG_LENGTH);
+    sprintf(HeaderMessageStr, "doSubtractDarkFullFrame(%d, %d)", numRows, numCols);
+    printPositiveTestHeader(stdout, "pmSubtractBias", HeaderMessageStr);
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            myReadout->image->data.F32[i][j] = (float) (i + j);
+            myDark->image->data.F32[i][j] = 1.0;
+        }
+    }
+
+    myReadout = pmSubtractBias(myReadout, NULL, PM_FIT_NONE, false,
+                               NULL, 0, NULL, myDark);
+
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            expect = ((float) (i + j)) - 1.0;
+            actual = myReadout->image->data.F32[i][j];
+            if (FLT_EPSILON < fabs(expect - actual)) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                testStatus = 1;
+            }
+        }
+    }
+
+
+    psFree(myReadout);
+    psFree(myDark);
+    printFooter(stdout, "pmSubtractBias", HeaderMessageStr, true);
+    psFree(HeaderMessageStr);
+    return(testStatus);
+}
+
+
+int test00b( void )
+{
+    int testStatus = 0;
+
+    testStatus |= doSubtractDarkFullFrame(1, 1);
+    testStatus |= doSubtractDarkFullFrame(NUM_COLS, 1);
+    testStatus |= doSubtractDarkFullFrame(1, NUM_ROWS);
+    testStatus |= doSubtractDarkFullFrame(NUM_COLS, NUM_ROWS);
+    return(testStatus);
+}
+
+
+/*
+int doSubtractOverscansTestInputCases(int numCols, int numRows)
+{
+    int i;
+    int j;
+    int testStatus = 0;
+    psImage *tmpImage1 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *tmpImage2 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *tmpImage3 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *tmpImage4 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *tmpImage2Short = psImageAlloc(numCols-1, numRows-1, PS_TYPE_F32);
+    psImage *tmpImage3Short = psImageAlloc(numCols-1, numRows-1, PS_TYPE_F32);
+    psImage *tmpImage4Short = psImageAlloc(numCols-1, numRows-1, PS_TYPE_F32);
+    pmReadout *myReadout = pmReadoutAlloc(NULL);
+    myReadout->image = tmpImage1;
+    pmReadout *rc = NULL;
+    psList *list;
+    psList *listShort;
+    psStats *stat = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+    psImage *tmpImage5 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    pmReadout *myBias = pmReadoutAlloc(NULL);
+    myBias->image = tmpImage5;
+    printPositiveTestHeader(stdout, "pmSubtractBias", "Testing input parameter error conditions");
+ 
+    psImage *tmpImage5ShortRows = psImageAlloc(numCols, numRows-1, PS_TYPE_F32);
+    pmReadout *myBiasShortRows = pmReadoutAlloc(NULL);
+    myBiasShortRows->image = tmpImage5ShortRows;
+    psImage *tmpImage5ShortCols = psImageAlloc(numCols-1, numRows, PS_TYPE_F32);
+    pmReadout *myBiasShortCols = pmReadoutAlloc(NULL);
+    myBiasShortCols->image = tmpImage5ShortCols;
+ 
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            myReadout->image->data.F32[i][j] = (float) (i + j);
+            tmpImage2->data.F32[i][j] = 3.0;
+            tmpImage3->data.F32[i][j] = 4.0;
+            tmpImage4->data.F32[i][j] = 5.0;
+            myBias->image->data.F32[i][j] = 1.0;
+        }
+    }
+    list = psListAlloc(tmpImage2);
+    psListAdd(list, PS_LIST_HEAD, tmpImage3);
+    psListAdd(list, PS_LIST_HEAD, tmpImage4);
+ 
+    for (i=0;i<numRows-1;i++) {
+        for (j=0;j<numCols-1;j++) {
+            tmpImage2Short->data.F32[i][j] = 3.0;
+            tmpImage3Short->data.F32[i][j] = 4.0;
+            tmpImage4Short->data.F32[i][j] = 5.0;
+        }
+    }
+    listShort = psListAlloc(tmpImage2Short);
+    psListAdd(listShort, PS_LIST_HEAD, tmpImage3Short);
+    psListAdd(listShort, PS_LIST_HEAD, tmpImage4Short);
+    for (i=0;i<numRows-1;i++) {
+        for (j=0;j<numCols;j++) {
+            myBiasShortRows->image->data.F32[i][j] = 1.0;
+        }
+    }
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols-1;j++) {
+            myBiasShortCols->image->data.F32[i][j] = 1.0;
+        }
+    }
+ 
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() with NULL overscan list and PM_OVERSCAN_ALL.  Should generate error.\n");
+    rc = pmSubtractBias(myReadout, NULL, NULL, PM_OVERSCAN_ALL, stat, 0, PM_FIT_NONE, NULL);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractBias() did not return input pmReadout.\n");
+        testStatus = false;
+    }
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() with NULL overscan list and PM_OVERSCAN_ROWS.  Should generate error.\n");
+    rc = pmSubtractBias(myReadout, NULL, NULL, PM_OVERSCAN_ROWS, stat, 0, PM_FIT_NONE, NULL);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractBias() did not return input pmReadout.\n");
+        testStatus = false;
+        psFree(rc);
+    }
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() with NULL overscan list and PM_OVERSCAN_COLUMNS.  Should generate error.\n");
+    rc = pmSubtractBias(myReadout, NULL, NULL, PM_OVERSCAN_COLUMNS, stat, 0, PM_FIT_NONE, NULL);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractBias() did not return input pmReadout.\n");
+        testStatus = false;
+        psFree(rc);
+    }
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() with non-NULL overscan list and PM_OVERSCAN_NONE.  Should generate warning.\n");
+    rc = pmSubtractBias(myReadout, NULL, list, PM_OVERSCAN_NONE, stat, 0, PM_FIT_NONE, myBias);
+ 
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            psF32 expect = ((float) (i + j)) - 1.0;
+            psF32 actual = rc->image->data.F32[i][j];
+            if (FLT_EPSILON < fabs(expect - actual)) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                testStatus = 1;
+            }
+ 
+            // Restore myReadout for next test.
+            myReadout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+ 
+    // XXX: This does not seem to be a requirement.
+    if (0) {
+        printf("------------------------------------------------------------------\n");
+        printf("Calling pmSubtractBias() with NULL overscan list and PM_OVERSCAN_NONE.  Should generate warning.\n");
+        rc = pmSubtractBias(myReadout, NULL, NULL, PM_OVERSCAN_NONE, stat,
+                            0, PM_FIT_NONE, myBias);
+ 
+        for (i=0;i<numRows;i++) {
+            for (j=0;j<numCols;j++) {
+                psF32 expect = ((float) (i + j)) - 1.0;
+                psF32 actual = rc->image->data.F32[i][j];
+                if (FLT_EPSILON < fabs(expect - actual)) {
+                    printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                    testStatus = 1;
+                }
+ 
+                // Restore myReadout for next test.
+                myReadout->image->data.F32[i][j] = (float) (i + j);
+            }
+        }
+    }
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() with PM_OVERSCAN_NONE and PM_FIT_POLYNOMIAL.  Should generate Warning.\n");
+    rc = pmSubtractBias(myReadout, NULL, NULL, PM_OVERSCAN_NONE, stat, 0, PM_FIT_POLYNOMIAL, myBias);
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            psF32 expect = ((float) (i + j)) - 1.0;
+            psF32 actual = rc->image->data.F32[i][j];
+            if (FLT_EPSILON < fabs(expect - actual)) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                testStatus = 1;
+            }
+ 
+            // Restore myReadout for next test.
+            myReadout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+ 
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() with PM_OVERSCAN_ALL and PM_FIT_SPLINE.  Should generate Warning.\n");
+    rc = pmSubtractBias(myReadout, NULL, NULL, PM_OVERSCAN_NONE, stat, 0, PM_FIT_SPLINE, myBias);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractBias() did not return input pmReadout.\n");
+        testStatus = false;
+        psFree(rc);
+    }
+ 
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            psF32 expect = ((float) (i + j)) - 1.0;
+            psF32 actual = rc->image->data.F32[i][j];
+            if (FLT_EPSILON < fabs(expect - actual)) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                testStatus = 1;
+            }
+ 
+            // Restore myReadout for next test.
+            myReadout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+ 
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() with multiple stats->options.  Should generate Warning.\n");
+    stat->options|= PS_STAT_SAMPLE_MEDIAN;
+    myReadout = pmSubtractBias(myReadout, NULL, list, PM_OVERSCAN_ALL, stat,
+                               0, PM_FIT_NONE, NULL);
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            psF32 expect = ((float) (i + j)) - 12.0;
+            psF32 actual = rc->image->data.F32[i][j];
+            if (FLT_EPSILON < fabs(expect - actual)) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                testStatus = 1;
+            }
+        }
+    }
+    stat->options = PS_STAT_SAMPLE_MEAN;
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() undersize overscans (PM_OVERSCAN_ROWS).  Should generate Warning.\n");
+    rc = pmSubtractBias(myReadout, NULL, listShort, PM_OVERSCAN_ROWS, stat,
+                        0, PM_FIT_NONE, NULL);
+    if (0) {
+        for (i=0;i<numRows;i++) {
+            for (j=0;j<numCols;j++) {
+                psF32 expect = ((float) (i + j)) - 12.0;
+                psF32 actual = rc->image->data.F32[i][j];
+                if (FLT_EPSILON < fabs(expect - actual)) {
+                    printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                    testStatus = 1;
+                }
+            }
+        }
+    }
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() undersize overscans (PM_OVERSCAN_COLUMNS).  Should generate Warning.\n");
+    rc = pmSubtractBias(myReadout, NULL, listShort, PM_OVERSCAN_COLUMNS, stat,
+                        0, PM_FIT_NONE, NULL);
+    if (0) {
+        for (i=0;i<numRows;i++) {
+            for (j=0;j<numCols;j++) {
+                psF32 expect = ((float) (i + j)) - 12.0;
+                psF32 actual = rc->image->data.F32[i][j];
+                if (FLT_EPSILON < fabs(expect - actual)) {
+                    printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                    testStatus = 1;
+                }
+            }
+        }
+    }
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() undersize bias image (short rows).  Should generate Error.\n");
+    myReadout = pmSubtractBias(myReadout, NULL, NULL, PM_OVERSCAN_NONE, NULL,
+                               0, PM_FIT_NONE, myBiasShortRows);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractBias() did not return input pmReadout.\n");
+        testStatus = false;
+        psFree(rc);
+    }
+ 
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() undersize bias image (short columns).  Should generate Error.\n");
+    myReadout = pmSubtractBias(myReadout, NULL, NULL, PM_OVERSCAN_NONE, NULL,
+                               0, PM_FIT_NONE, myBiasShortCols);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractBias() did not return input pmReadout.\n");
+        testStatus = false;
+        psFree(rc);
+    }
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() with bogus PM_FIT.  Should generate Error.\n");
+    myReadout = pmSubtractBias(myReadout, NULL, list, PM_OVERSCAN_ROWS, stat,
+                               0, 54321, NULL);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractBias() did not return input pmReadout.\n");
+        testStatus = false;
+        psFree(rc);
+    }
+ 
+    printf("------------------------------------------------------------------\n");
+    printf("Calling pmSubtractBias() with bogus overScanAxis.  Should generate Error.\n");
+    myReadout = pmSubtractBias(myReadout, NULL, list, 54321, stat,
+                               0, PM_FIT_NONE, NULL);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractBias() did not return input pmReadout.\n");
+        testStatus = false;
+        psFree(rc);
+    }
+ 
+    if (0) {
+        for (i=0;i<numRows;i++) {
+            for (j=0;j<numCols;j++) {
+                psF32 expect = ((float) (i + j)) - 12.0;
+                psF32 actual = rc->image->data.F32[i][j];
+                if (FLT_EPSILON < fabs(expect - actual)) {
+                    printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                    testStatus = 1;
+                }
+            }
+        }
+    }
+ 
+    printf("------------------------------------------------------------------\n");
+    psFree(myReadout);
+    psFree(tmpImage2);
+    psFree(tmpImage3);
+    psFree(tmpImage4);
+    psFree(tmpImage2Short);
+    psFree(tmpImage3Short);
+    psFree(tmpImage4Short);
+    psFree(myBias);
+    psFree(myBiasShortRows);
+    psFree(myBiasShortCols);
+    psFree(stat);
+    psFree(list);
+    psFree(listShort);
+ 
+    printFooter(stdout, "pmSubtractBias", "Testing input parameter error conditions", true);
+    return(testStatus);
+}
+ 
+int test04( void )
+{
+    int testStatus = 0;
+ 
+    testStatus |= doSubtractOverscansTestInputCases(NUM_COLS, NUM_ROWS);
+    return(testStatus);
+}
+*/
+
+void PS_POLY1D_PRINT(
+    psPolynomial1D *poly)
+{
+    printf("-------------- PS_POLY1D_PRINT() --------------\n");
+    printf("poly->nX is %d\n", poly->nX);
+    for (psS32 i = 0 ; i < (1 + poly->nX) ; i++) {
+        printf("poly->coeff[%d] is %f\n", i, poly->coeff[i]);
+    }
+}
+
+void PS_PRINT_SPLINE2(psSpline1D *mySpline)
+{
+    printf("-------------- PS_PRINT_SPLINE2() --------------\n");
+    if (mySpline != NULL) {
+        printf("mySpline->n is %d\n", mySpline->n);
+        for (psS32 i = 0 ; i < mySpline->n ; i++) {
+            if (mySpline->spline[i] != NULL) {
+                PS_POLY1D_PRINT(mySpline->spline[i]);
+            }
+        }
+        if (mySpline->knots != NULL) {
+            PS_VECTOR_PRINT_F32(mySpline->knots);
+        }
+    } else {
+        printf("NULL\n");
+    }
+    printf("-------------- PS_PRINT_SPLINE2() DONE --------------\n");
+}
+
+
+
+
+
+
+/******************************************************************************
+doSubtractOverscansGeneric(): This is a general version of the
+bias subtraction tests which allows the various parameters to be specified
+as arguments.
+ *****************************************************************************/
+int doSubtractOverscansGeneric(
+    int imageNumCols,
+    int imageNumRows,
+    int overscanNumCols,
+    int overscanNumRows,
+    int numOverscans,
+    pmOverscanAxis overscanaxis,
+    pmFit fit,
+    psS32 nBin)
+{
+    int i;
+    int j;
+    float actual;
+    float expect;
+    int testStatus = 0;
+
+    printPositiveTestHeader(stdout, "pmSubtractBias", "PUT COMMENT HERE");
+    printf("---- doSubtractOverscansGeneric() ----\n");
+    printf("    Image size: %d by %d\n", imageNumRows, imageNumCols);
+    printf("    Overscan size: %d by %d\n", overscanNumRows, overscanNumCols);
+    printf("    Total Overscans: %d\n", numOverscans);
+    printf("    Binning factor: %d\n", nBin);
+    if (overscanaxis == PM_OVERSCAN_ROWS)
+        printf("    Overscan axis: PM_OVERSCAN_ROWS\n");
+    if (overscanaxis == PM_OVERSCAN_COLUMNS)
+        printf("    Overscan axis: PM_OVERSCAN_COLUMNS\n");
+    if (overscanaxis == PM_OVERSCAN_ALL)
+        printf("    Overscan axis: PM_OVERSCAN_ALL\n");
+    if (overscanaxis == PM_OVERSCAN_NONE)
+        printf("    Overscan axis: PM_OVERSCAN_NONE\n");
+    if (fit == PM_FIT_NONE)
+        printf("    Fit type: PM_FIT_NONE\n");
+    if (fit == PM_FIT_POLYNOMIAL)
+        printf("    Fit type: PM_FIT_POLYNOMIAL\n");
+    if (fit == PM_FIT_SPLINE)
+        printf("    Fit type: PM_FIT_SPLINE\n");
+
+    //
+    // Create and initialize input image, FPA hierarchy.
+    //
+    const psMetadata *camera = psMetadataAlloc();
+    pmFPA* fpa = pmFPAAlloc(camera);
+
+    if (fpa == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmFPAAlloc returned a NULL.\n");
+        return 1;
+    }
+
+    pmChip *chip = pmChipAlloc(fpa, "ChipName");
+    if (chip == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmChipAlloc returned a NULL.\n");
+        return 2;
+    }
+
+    pmCell *cell = pmCellAlloc(chip, (psMetadata *) camera, "CellName");
+    if (cell == NULL) {
+        psLogMsg(__func__,PS_LOG_ERROR, "TEST ERROR: pmCellAlloc returned a NULL.\n");
+        return 3;
+    }
+
+    pmReadout *myReadout = pmReadoutAlloc(cell);
+    myReadout->image = psImageAlloc(imageNumCols, imageNumRows, PS_TYPE_F32);
+    for (i=0;i<myReadout->image->numRows;i++) {
+        for (j=0;j<myReadout->image->numCols;j++) {
+            myReadout->image->data.F32[i][j] = (float) (i + j);
+        }
+    }
+
+    //
+    // Set overscan axis in the metadata.
+    //
+    psBool rc = false;
+    if (overscanaxis == PM_OVERSCAN_ROWS) {
+        rc = psMetadataAddS32(myReadout->parent->concepts, PS_LIST_HEAD, "CELL.READDIR", 0, NULL, 1);
+    } else if (overscanaxis == PM_OVERSCAN_COLUMNS) {
+        rc = psMetadataAddS32(myReadout->parent->concepts, PS_LIST_HEAD, "CELL.READDIR", 0, NULL, 2);
+    } else if (overscanaxis == PM_OVERSCAN_ALL) {
+        rc = psMetadataAddS32(myReadout->parent->concepts, PS_LIST_HEAD, "CELL.READDIR", 0, NULL, 3);
+    } else if (overscanaxis == PM_OVERSCAN_NONE) {
+        rc = psMetadataAddS32(myReadout->parent->concepts, PS_LIST_HEAD, "CELL.READDIR", 0, NULL, 0);
+    }
+    if (rc == false) {
+        printf("TEST ERROR: Could not set CELL.READDIR metadata.\n");
+        testStatus = 1;
+    }
+
+    psStats *stat = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+    psPolynomial1D *myPoly = psPolynomial1DAlloc(PS_POLYNOMIAL_ORD, POLYNOMIAL_FIT_ORDER);
+    psSpline1D *mySpline = NULL;
+
+
+    if (0) {
+        if (overscanNumRows <= 0) {
+            overscanNumRows = 1;
+        }
+        if (overscanNumCols <= 0) {
+            overscanNumCols = 1;
+        }
+    }
+    psF32 oAverage = 0.0;
+    myReadout->bias = NULL;
+    for (psS32 i = 0 ; i < numOverscans ; i++) {
+        psImage *tmpImage = psImageAlloc(overscanNumCols, overscanNumRows, PS_TYPE_F32);
+        psF32 oValue = (float) (i + 3);
+        PS_IMAGE_SET_F32(tmpImage, oValue);
+        oAverage += oValue;
+        if (myReadout->bias == NULL) {
+            myReadout->bias = psListAlloc(tmpImage);
+        } else {
+            psListAdd(myReadout->bias, PS_LIST_HEAD, tmpImage);
+        }
+    }
+    oAverage/= (psF32) numOverscans;
+    if (0) {
+        if (fit == PM_FIT_NONE) {
+            myReadout = pmSubtractBias(myReadout, NULL, PM_FIT_NONE, overscanaxis,
+                                       stat, nBin, NULL, NULL);
+        } else if (fit == PM_FIT_POLYNOMIAL) {
+            myReadout = pmSubtractBias(myReadout, myPoly, PM_FIT_POLYNOMIAL, overscanaxis,
+                                       stat, nBin, NULL, NULL);
+        } else if (fit == PM_FIT_SPLINE) {
+            //        mySpline = psSpline1DAlloc();
+            myReadout = pmSubtractBias(myReadout, mySpline, PM_FIT_SPLINE, overscanaxis,
+                                       stat, nBin, NULL, NULL);
+        }
+        if (myReadout == NULL ) {
+            printf("TEST ERROR: pmSubtractBias() returned NULL.\n");
+            testStatus = 1;
+        } else {
+            for (i=0;i<imageNumRows;i++) {
+                for (j=0;j<imageNumCols;j++) {
+                    expect = ((float) (i + j)) - oAverage;
+                    actual = myReadout->image->data.F32[i][j];
+                    if (FLT_EPSILON < fabs(expect - actual)) {
+                        printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                        testStatus = 1;
+                    } else {
+                        //printf("GOOD: image[%d][%d] is %f, should be %f\n", i, j, actual, expect);
+                    }
+                }
+            }
+        }
+    }
+
+    // HEY
+    psFree(fpa);
+    psFree(stat);
+    psFree(myPoly);
+    psFree(mySpline);
+
+    printFooter(stdout, "pmSubtractBias", "Column Overscans", true);
+    return(testStatus);
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/******************************************************************************
+test05a() The following combinations are tested here:
+ Overscan images are same size, no fit, bin factor is 1.
+ Overscan images are same size, no fit, bin factor is 2.
+ *****************************************************************************/
+int test05a(
+    psS32 imageNumCols,
+    psS32 imageNumRows,
+    psS32 overscanNumCols,
+    psS32 overscanNumRows)
+{
+    int testStatus = 0;
+
+    // imageNumCols, imageNumRows, overscanNumCols, overscanNumRows,
+    // overscanaxis, fit, nBin
+
+    //
+    // Overscan images are same size, no fit, bin factor is 1.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_NONE, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_NONE, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_NONE, 1);
+
+    //
+    // Overscan images are same size, no fit, bin factor is 2.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_NONE, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_NONE, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_NONE, 2);
+
+    return(testStatus);
+}
+
+
+/******************************************************************************
+test05b() The following combinations are tested here:
+ Overscan images are too small, spline fit, bin factor is 1.
+ Overscan images are too small, spline fit, bin factor is 2.
+ Overscan images are same size, spline fit, bin factor is 1.
+ Overscan images are same size, spline fit, bin factor is 2.
+ Overscan images are too big,   spline fit, bin factor is 1.
+ Overscan images are too big,   spline fit, bin factor is 2.
+ A single overscan image of the same size, spline fit, bin factor is 1.
+ 
+ Overscan images are too small, polynomial fit, bin factor is 1.
+ Overscan images are too small, polynomial fit, bin factor is 2.
+ Overscan images are same size, polynomial fit, bin factor is 1.
+ Overscan images are same size, polynomial fit, bin factor is 2.
+ Overscan images are too big,   polynomial fit, bin factor is 1.
+ Overscan images are too big,   polynomial fit, bin factor is 2.
+ A single overscan image of the same size, polynomial fit, bin factor is 1.
+ 
+XXX: Must add M-by-N image size tests.
+ *****************************************************************************/
+int test05b(
+    psS32 imageNumCols,
+    psS32 imageNumRows,
+    psS32 overscanNumCols,
+    psS32 overscanNumRows)
+{
+    int testStatus = 0;
+
+    //
+    // Overscan images are too small, spline fit, bin factor is 1.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_SPLINE, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_SPLINE, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_SPLINE, 1);
+
+    //
+    // Overscan images are too small, spline fit, bin factor is 2.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_SPLINE, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_SPLINE, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_SPLINE, 2);
+
+
+    //
+    // Overscan images are same size, spline fit, bin factor is 1.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_SPLINE, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_SPLINE, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_SPLINE, 1);
+
+    //
+    // Overscan images are same size, spline fit, bin factor is 2.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_SPLINE, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_SPLINE, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_SPLINE, 2);
+
+    //
+    // Overscan images are too big, spline fit, bin factor is 1.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_SPLINE, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_SPLINE, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_SPLINE, 1);
+
+    //
+    // Overscan images are too big, spline fit, bin factor is 2.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_SPLINE, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_SPLINE, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2 , NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_SPLINE, 2);
+
+
+    //
+    // A single overscan image of the same size, spline fit, bin factor is 1.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, 1,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_SPLINE, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, 1,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_SPLINE, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, 1,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_SPLINE, 1);
+
+
+    //
+    // Overscan images are too small, polynomial fit, bin factor is 1.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    //
+    // Overscan images are too small, polynomial fit, bin factor is 2.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_POLYNOMIAL, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_POLYNOMIAL, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols-2, overscanNumRows-2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_POLYNOMIAL, 2);
+
+
+    //
+    // Overscan images are same size, polynomial fit, bin factor is 1.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    //
+    // Overscan images are same size, polynomial fit, bin factor is 2.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_POLYNOMIAL, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_POLYNOMIAL, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_POLYNOMIAL, 2);
+
+    //
+    // Overscan images are too big, polynomial fit, bin factor is 1.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    //
+    // Overscan images are too big, polynomial fit, bin factor is 2.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_POLYNOMIAL, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2, NUM_OVERSCANS,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_POLYNOMIAL, 2);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols+2, overscanNumRows+2, NUM_OVERSCANS,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_POLYNOMIAL, 2);
+
+    //
+    // A single overscan image of the same size, polynomial fit, bin factor is 1.
+    //
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, 1,
+                  PM_OVERSCAN_ALL,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, 1,
+                  PM_OVERSCAN_COLUMNS,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    testStatus |= doSubtractOverscansGeneric(imageNumCols, imageNumRows,
+                  overscanNumCols, overscanNumRows, 1,
+                  PM_OVERSCAN_ROWS,
+                  PM_FIT_POLYNOMIAL, 1);
+
+    return(testStatus);
+}
+
+
+
+#define LOW_COLS 3
+#define LOW_ROWS 3
+
+/******************************************************************************
+test05(): See test05a() and test05b().
+ 
+We run the tests in test05b() starting with all possible combinations of
+sizes.
+ 
+XXX: Must add M-by-N image size tests.
+ *****************************************************************************/
+int test05()
+{
+    int testStatus = 0;
+    //    testStatus = test05a(NUM_COLS, NUM_ROWS, NUM_COLS, NUM_ROWS);
+
+    //    testStatus|= test05b(LOW_COLS, LOW_ROWS, LOW_COLS, LOW_ROWS);
+    //    testStatus|= test05b(LOW_COLS, LOW_ROWS, LOW_COLS, NUM_ROWS);
+    //    testStatus|= test05b(LOW_COLS, LOW_ROWS, NUM_COLS, LOW_ROWS);
+    //    testStatus|= test05b(LOW_COLS, LOW_ROWS, NUM_COLS, NUM_ROWS);
+
+    //    testStatus|= test05b(LOW_COLS, NUM_ROWS, LOW_COLS, LOW_ROWS);
+    //    testStatus|= test05b(LOW_COLS, NUM_ROWS, LOW_COLS, NUM_ROWS);
+    //    testStatus|= test05b(LOW_COLS, NUM_ROWS, NUM_COLS, LOW_ROWS);
+    //    testStatus|= test05b(LOW_COLS, NUM_ROWS, NUM_COLS, NUM_ROWS);
+
+    //    testStatus|= test05b(NUM_COLS, LOW_ROWS, LOW_COLS, LOW_ROWS);
+    //    testStatus|= test05b(NUM_COLS, LOW_ROWS, LOW_COLS, NUM_ROWS);
+    //    testStatus|= test05b(NUM_COLS, LOW_ROWS, NUM_COLS, LOW_ROWS);
+    //    testStatus|= test05b(NUM_COLS, LOW_ROWS, NUM_COLS, NUM_ROWS);
+
+    //    testStatus|= test05b(NUM_COLS, NUM_ROWS, LOW_COLS, LOW_ROWS);
+    //    testStatus|= test05b(NUM_COLS, NUM_ROWS, LOW_COLS, NUM_ROWS);
+    //    testStatus|= test05b(NUM_COLS, NUM_ROWS, NUM_COLS, LOW_ROWS);
+    testStatus|= test05b(NUM_COLS, NUM_ROWS, NUM_COLS, NUM_ROWS);
+
+
+    return(testStatus);
+}
+
Index: /branches/eam_branch_20071023/psModules/test/detrend/tst_pmSubtractSky.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/detrend/tst_pmSubtractSky.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/detrend/tst_pmSubtractSky.c	(revision 15358)
@@ -0,0 +1,375 @@
+/** @file tst_pmSubtractSky.c
+ *
+ *  @brief Contains the tests for pmSubtractSky.c:
+ *
+ * test00: This code will test the pmSubtractSky routine.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-05-25 22:02:23 $
+ *
+ *  XXX: I added the CELL.TRIMSEC region code but there are not tests for it.
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+#include "psTest.h"
+#include "pslib.h"
+#include "pmSubtractSky.h"
+#define NUM_ROWS 512
+#define NUM_COLS 512
+#define POLY_X_ORDER 3
+#define POLY_Y_ORDER 3
+#define ERROR_TOLERANCE 1.0
+#define OBJECT_INTENSITY 2000.0
+static int test00(void);
+static int test01(void);
+testDescription tests[] = {
+                              {test00, 000, "pmSubtractSky", 0, false},
+                              {test01, 000, "pmSubtractSky: warning, error messages", 0, false},
+                              {NULL}
+                          };
+
+float func(int i, int j)
+{
+    return((float) (i + j));
+}
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    return !runTestSuite(stderr, "Test Point Driver", tests, argc, argv);
+    //    test00();
+}
+
+/******************************************************************************
+ *****************************************************************************/
+int doSubtractSkySimple(int numCols, int numRows, int binFactor)
+{
+    int i;
+    int j;
+    int testStatus = 0;
+    psImage *tmpImageF32 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    //    pmReadout *myReadout = pmReadoutAlloc(numCols, numRows, tmpImageF32);
+    pmReadout *myReadout = pmReadoutAlloc(NULL);
+    myReadout->image = tmpImageF32;
+    psStats *myStats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+    psPolynomial2D *myPoly = psPolynomial2DAlloc(PS_POLYNOMIAL_ORD, POLY_X_ORDER, POLY_Y_ORDER);
+
+    printPositiveTestHeader(stdout, "pmSubtractSky", "doSubtractSkySimple");
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            myReadout->image->data.F32[i][j] = func(i, j);
+        }
+    }
+
+    myReadout = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_POLYNOMIAL,
+                              binFactor, myStats, 10.0);
+    if (myReadout == NULL) {
+        printf("TEST ERROR: pmSubtractSky() returned NULL.\n");
+        testStatus = 1;
+    } else {
+        for (i=0;i<numRows;i++) {
+            for (j=0;j<numCols;j++) {
+                if (ERROR_TOLERANCE < fabs(myReadout->image->data.F32[i][j])) {
+                    printf("TEST ERROR: image[%d][%d] is %f, should be 0.0\n", i, j, myReadout->image->data.F32[i][j]);
+                    testStatus = 1;
+                }
+            }
+        }
+    }
+    psFree(myReadout);
+    psFree(myStats);
+    psFree(myPoly);
+    printFooter(stdout, "pmSubtractSky", "doSubtractSkySimple", true);
+    return(testStatus);
+}
+
+/******************************************************************************
+ *****************************************************************************/
+int doSubtractSkyWithObjects(int numCols, int numRows, int binFactor)
+{
+    int i;
+    int j;
+    int testStatus = 0;
+    psImage *tmpImageF32 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    //    pmReadout *myReadout = pmReadoutAlloc(numCols, numRows, tmpImageF32);
+    pmReadout *myReadout = pmReadoutAlloc(NULL);
+    myReadout->image = tmpImageF32;
+    psStats *myStats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+    psPolynomial2D *myPoly = psPolynomial2DAlloc(PS_POLYNOMIAL_ORD, POLY_X_ORDER, POLY_Y_ORDER);
+    psImage *trueImage = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psF32 errorTolerance = ERROR_TOLERANCE * ((psF32) binFactor);
+
+    printPositiveTestHeader(stdout, "pmSubtractSky", "doSubtractSkyWithObjects");
+    for (i=0;i<numRows;i++) {
+        for (j=0;j<numCols;j++) {
+            myReadout->image->data.F32[i][j] = func(i, j);
+        }
+    }
+    // We insert a few bright spots in the image.
+    myReadout->image->data.F32[NUM_ROWS/4][NUM_COLS/4]+= OBJECT_INTENSITY;
+    myReadout->image->data.F32[NUM_ROWS/4][3*NUM_COLS/4]+= OBJECT_INTENSITY;
+    myReadout->image->data.F32[3*NUM_ROWS/4][NUM_COLS/4]+= OBJECT_INTENSITY;
+    myReadout->image->data.F32[3*NUM_ROWS/4][3*NUM_COLS/4]+= OBJECT_INTENSITY;
+    PS_IMAGE_SET_F32(trueImage, 0.0);
+    trueImage->data.F32[NUM_ROWS/4][NUM_COLS/4]+= OBJECT_INTENSITY;
+    trueImage->data.F32[NUM_ROWS/4][3*NUM_COLS/4]+= OBJECT_INTENSITY;
+    trueImage->data.F32[3*NUM_ROWS/4][NUM_COLS/4]+= OBJECT_INTENSITY;
+    trueImage->data.F32[3*NUM_ROWS/4][3*NUM_COLS/4]+= OBJECT_INTENSITY;
+
+    myReadout = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_POLYNOMIAL,
+                              binFactor, myStats, 2.0);
+    if (myReadout == NULL) {
+        printf("TEST ERROR: pmSubtractSky() returned NULL.\n");
+        testStatus = 1;
+    } else {
+        for (i=0;i<numRows;i++) {
+            for (j=0;j<numCols;j++) {
+                if (errorTolerance < fabs(myReadout->image->data.F32[i][j] - trueImage->data.F32[i][j])) {
+                    printf("TEST ERROR: image[%d][%d] is %f, should be %f\n", i, j,
+                           myReadout->image->data.F32[i][j], trueImage->data.F32[i][j]);
+                    testStatus = 1;
+                }
+            }
+        }
+    }
+    psFree(myReadout);
+    psFree(myStats);
+    psFree(myPoly);
+    psFree(trueImage);
+    printFooter(stdout, "pmSubtractSky", "doSubtractSkyWithObjects", true);
+    return(testStatus);
+}
+
+
+int test00( void )
+{
+    int testStatus = 0;
+
+    psTraceSetLevel(".", 0);
+
+    // Bin Factor == 1
+    printf("doSubtractSkySimple(1, 1, 1)\n");
+    testStatus |= doSubtractSkySimple(1, 1, 1);
+    printf("doSubtractSkySimple(NUM_COLS, 1, 1)\n");
+    testStatus |= doSubtractSkySimple(NUM_COLS, 1, 1);
+
+    printf("doSubtractSkySimple(1, NUM_ROWS, 1)\n");
+    testStatus |= doSubtractSkySimple(1, NUM_ROWS, 1);
+    printf("doSubtractSkySimple(NUM_COLS, NUM_ROWS, 1)\n");
+    testStatus |= doSubtractSkySimple(NUM_COLS, NUM_ROWS, 1);
+
+    // Bin Factor == 2
+    printf("doSubtractSkySimple(1, 1, 2)\n");
+    testStatus |= doSubtractSkySimple(1, 1, 2);
+    printf("doSubtractSkySimple(NUM_COLS, 1, 2)\n");
+    testStatus |= doSubtractSkySimple(NUM_COLS, 1, 2);
+    printf("doSubtractSkySimple(1, NUM_ROWS, 2)\n");
+    testStatus |= doSubtractSkySimple(1, NUM_ROWS, 2);
+    printf("doSubtractSkySimple(NUM_COLS, NUM_ROWS, 2)\n");
+    testStatus |= doSubtractSkySimple(NUM_COLS, NUM_ROWS, 2);
+
+    printf("doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 1)\n");
+    testStatus |= doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 1);
+    printf("doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 2)\n");
+    testStatus |= doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 2);
+    printf("doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 4)\n");
+    testStatus |= doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 8);
+
+    printf("doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 8)\n");
+    testStatus |= doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 8);
+    printf("doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 16)\n");
+    testStatus |= doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 16);
+    printf("doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 32)\n");
+    testStatus |= doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 32);
+    printf("doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 64)\n");
+    testStatus |= doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 64);
+    printf("doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 128)\n");
+    testStatus |= doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 128);
+    printf("doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 256)\n");
+    testStatus |= doSubtractSkyWithObjects(NUM_COLS, NUM_ROWS, 256);
+
+    return(testStatus);
+}
+
+#define NUM_ROWS_SMALL 16
+#define NUM_COLS_SMALL 16
+int test01( void )
+{
+    int testStatus = 0;
+    psS32 i;
+    psS32 j;
+    psImage *tmpImageF32 = psImageAlloc(NUM_COLS_SMALL, NUM_ROWS_SMALL, PS_TYPE_F32);
+    psImage *tmpImageF64 = psImageAlloc(NUM_COLS_SMALL, NUM_ROWS_SMALL, PS_TYPE_F64);
+    //    pmReadout *myReadout = pmReadoutAlloc(NUM_COLS_SMALL, NUM_ROWS_SMALL, tmpImageF32);
+    pmReadout *myReadout = pmReadoutAlloc(NULL);
+    myReadout->image = tmpImageF32;
+    pmReadout *rc = NULL;
+    psStats *myStats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+    psPolynomial2D *myPoly = psPolynomial2DAlloc(PS_POLYNOMIAL_ORD, POLY_X_ORDER, POLY_Y_ORDER);
+
+    printPositiveTestHeader(stdout, "pmSubtractSky", "Testing bad input parameter conditions.");
+    for (i=0;i<NUM_ROWS_SMALL;i++) {
+        for (j=0;j<NUM_COLS_SMALL;j++) {
+            myReadout->image->data.F32[i][j] = func(i, j);
+        }
+    }
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmSubtractSky() with NULL pmReadout.  Should error.\n\n");
+    rc = pmSubtractSky(NULL, (void *) myPoly, PM_FIT_POLYNOMIAL, 1, myStats, 2.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmSubtractSky() returned a non-NULL pmReadout\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmSubtractSky() with NULL pmReadout->image.  Should error.\n\n");
+    myReadout->image = NULL;
+    rc = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_POLYNOMIAL, 1, myStats, 2.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmSubtractSky() returned a non-NULL pmReadout\n");
+        testStatus = false;
+    }
+    myReadout->image = tmpImageF32;
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmSubtractSky() with PS_TYPE_F64 pmReadout->image.  Should error.\n\n");
+    myReadout->image = tmpImageF64;
+    rc = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_POLYNOMIAL, 1, myStats, 2.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmSubtractSky() returned a non-NULL pmReadout\n");
+        testStatus = false;
+    }
+    myReadout->image = tmpImageF32;
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmSubtractSky() with NULL fitSpec.  Should return image, no ERROR, no WARNING.\n\n");
+    rc = pmSubtractSky(myReadout, NULL, PM_FIT_POLYNOMIAL, 1, myStats, 2.0);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractSky() returned something other than pmReadout\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmSubtractSky() with PM_FIT_NONE fit.  Should return image, no ERROR, no WARNING.\n\n");
+    rc = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_NONE, 1, myStats, 2.0);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractSky() returned something other than pmReadout\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmSubtractSky() with PM_FIT_SPLINE fit.  Should return image, no ERROR, no WARNING.\n\n");
+    rc = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_SPLINE, 1, myStats, 2.0);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractSky() returned something other than pmReadout\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmSubtractSky() with NULL myStats.  Should fit entire image, generate WARNING.\n\n");
+    rc = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_POLYNOMIAL, 1, NULL, 2.0);
+    for (i=0;i<NUM_ROWS_SMALL;i++) {
+        for (j=0;j<NUM_COLS_SMALL;j++) {
+            if (ERROR_TOLERANCE < fabs(rc->image->data.F32[i][j])) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be 0.0\n", i, j,
+                       rc->image->data.F32[i][j]);
+                testStatus = false;
+            }
+        }
+    }
+
+    printf("----------------------------------------------------------------\n");
+    psU64 oldOptions = myStats->options;
+    myStats->options = 0;
+    printf("Calling pmSubtractSky() with no myStats->options specified.  Should fit entire image, generate WARNING.\n\n");
+    rc = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_POLYNOMIAL, 1, myStats, 2.0);
+    for (i=0;i<NUM_ROWS_SMALL;i++) {
+        for (j=0;j<NUM_COLS_SMALL;j++) {
+            if (ERROR_TOLERANCE < fabs(rc->image->data.F32[i][j])) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be 0.0\n", i, j,
+                       rc->image->data.F32[i][j]);
+                testStatus = false;
+            }
+        }
+    }
+    myStats->options = oldOptions;
+
+    printf("----------------------------------------------------------------\n");
+    oldOptions = myStats->options;
+    myStats->options = PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_MEDIAN;
+    printf("Calling pmSubtractSky() with multiple myStats->options specified.  Should fit entire image, generate WARNING.\n\n");
+    rc = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_POLYNOMIAL, 1, myStats, 2.0);
+    for (i=0;i<NUM_ROWS_SMALL;i++) {
+        for (j=0;j<NUM_COLS_SMALL;j++) {
+            if (ERROR_TOLERANCE < fabs(rc->image->data.F32[i][j])) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be 0.0\n", i, j,
+                       rc->image->data.F32[i][j]);
+                testStatus = false;
+            }
+        }
+    }
+    myStats->options = oldOptions;
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmSubtractSky() with 0 binFactor.  Should fit entire image, generate WARNING.\n\n");
+    rc = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_POLYNOMIAL, 0, myStats, 2.0);
+    for (i=0;i<NUM_ROWS_SMALL;i++) {
+        for (j=0;j<NUM_COLS_SMALL;j++) {
+            if (ERROR_TOLERANCE < fabs(rc->image->data.F32[i][j])) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be 0.0\n", i, j,
+                       rc->image->data.F32[i][j]);
+                testStatus = false;
+            }
+        }
+    }
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmSubtractSky() with -1 binFactor.  Should fit entire image, generate WARNING.\n\n");
+    rc = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_POLYNOMIAL, -1, myStats, 2.0);
+    for (i=0;i<NUM_ROWS_SMALL;i++) {
+        for (j=0;j<NUM_COLS_SMALL;j++) {
+            if (ERROR_TOLERANCE < fabs(rc->image->data.F32[i][j])) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be 0.0\n", i, j,
+                       rc->image->data.F32[i][j]);
+                testStatus = false;
+            }
+        }
+    }
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmSubtractSky() with -1.0 clipSD.  Should fit entire image, generate WARNING.\n\n");
+    rc = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_POLYNOMIAL, 1, myStats, -1.0);
+    for (i=0;i<NUM_ROWS_SMALL;i++) {
+        for (j=0;j<NUM_COLS_SMALL;j++) {
+            if (ERROR_TOLERANCE < fabs(rc->image->data.F32[i][j])) {
+                printf("TEST ERROR: image[%d][%d] is %f, should be 0.0\n", i, j,
+                       rc->image->data.F32[i][j]);
+                testStatus = false;
+            }
+        }
+    }
+
+    printf("----------------------------------------------------------------\n");
+    printf("Calling pmSubtractSky() with bogus psFit.  Should generate Error.\n\n");
+    rc = pmSubtractSky(myReadout, (void *) myPoly, 54321, 1, myStats, -1.0);
+    if (rc != myReadout) {
+        printf("TEST ERROR: pmSubtractSky() returned something other than pmReadout\n");
+        testStatus = false;
+    }
+
+
+    //    myReadout = pmSubtractSky(myReadout, (void *) myPoly, PM_FIT_POLYNOMIAL,
+    //                              1, myStats, 2.0);
+
+
+    printf("----------------------------------------------------------------\n");
+    psFree(myReadout);
+    psFree(myStats);
+    psFree(myPoly);
+    psFree(tmpImageF64);
+    printFooter(stdout, "pmSubtractSky", "Testing bad input parameter conditions.", true);
+    return(testStatus);
+}
Index: /branches/eam_branch_20071023/psModules/test/extras/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/extras/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/extras/.cvsignore	(revision 15358)
@@ -0,0 +1,2 @@
+Makefile
+Makefile.in
Index: /branches/eam_branch_20071023/psModules/test/extras/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/extras/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/extras/Makefile.am	(revision 15358)
@@ -0,0 +1,25 @@
+AM_CPPFLAGS = \
+	$(SRCINC) \
+	-I$(top_srcdir)/test/tap/src \
+	-I$(top_srcdir)/test/pstap/src \
+	$(PSMODULES_CFLAGS)
+
+AM_LDFLAGS = \
+	$(top_builddir)/src/libpsmodules.la  \
+	$(top_builddir)/test/tap/src/libtap.la \
+	$(top_builddir)/test/pstap/src/libpstap.la \
+	$(PSMODULES_LIBS)
+
+TEST_PROGS =
+
+if BUILD_TESTS
+bin_PROGRAMS = $(TEST_PROGS)
+TESTS = $(TEST_PROGS)
+else
+check_PROGRAMS = $(TEST_PROGS)
+endif
+
+CLEANFILES = $(check_DATA) temp/* core core.* *~ *.bb *.bbg *.da gmon.out
+
+test: check
+	$(top_srcdir)/test/test.pl
Index: /branches/eam_branch_20071023/psModules/test/imcombine/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/imcombine/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/imcombine/.cvsignore	(revision 15358)
@@ -0,0 +1,8 @@
+.deps
+.libs
+Makefile
+Makefile.in
+temp
+tst_pmImageCombine
+tst_pmReadoutCombine
+tap_pmImageCombine
Index: /branches/eam_branch_20071023/psModules/test/imcombine/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/imcombine/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/imcombine/Makefile.am	(revision 15358)
@@ -0,0 +1,26 @@
+AM_CPPFLAGS = \
+	$(SRCINC) \
+	-I$(top_srcdir)/test/tap/src \
+	-I$(top_srcdir)/test/pstap/src \
+	$(PSMODULES_CFLAGS)
+
+AM_LDFLAGS = \
+	$(top_builddir)/src/libpsmodules.la  \
+	$(top_builddir)/test/tap/src/libtap.la \
+	$(top_builddir)/test/pstap/src/libpstap.la \
+	$(PSMODULES_LIBS)
+
+TEST_PROGS = \
+	tap_pmImageCombine
+
+if BUILD_TESTS
+bin_PROGRAMS = $(TEST_PROGS)
+TESTS = $(TEST_PROGS)
+else
+check_PROGRAMS = $(TEST_PROGS)
+endif
+
+CLEANFILES = $(check_DATA) temp/* core core.* *~ *.bb *.bbg *.da gmon.out
+
+test: check
+	$(top_srcdir)/test/test.pl
Index: /branches/eam_branch_20071023/psModules/test/imcombine/tap_pmImageCombine.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/imcombine/tap_pmImageCombine.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/imcombine/tap_pmImageCombine.c	(revision 15358)
@@ -0,0 +1,67 @@
+#include <stdio.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+
+#define NUM 10
+#define SIZE 100
+
+psArray *generate_images(void)
+{
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 12345);   // Random Number Generator
+
+    // Generate images
+    psArray *images = psArrayAlloc(NUM);// Array of images
+    for (int i = 0; i < NUM; i++) {
+        psImage *image = psImageAlloc(SIZE, SIZE, PS_TYPE_F32); // Image i
+        for (int y = 0; y < SIZE; y++) {
+            for (int x = 0; x < SIZE; x++) {
+                image->data.F32[y][x] = psRandomGaussian(rng);
+            }
+        }
+        images->data[i] = image;
+    }
+    psFree(rng);
+
+    return images;
+}
+
+int main(int argc, char *argv[])
+{
+    plan_tests(6);
+
+    diag("Image combination tests");
+
+    // Basic combination
+    {
+        psArray *images = generate_images();
+        psImage *combined = pmCombineImages(NULL, NULL, images, NULL, NULL, 0, NULL, 1, 3.0);
+        ok(combined, "Combined image generated");
+        skip_start(!combined, 5, "Combination failed.");
+
+        ok(combined->type.type == PS_TYPE_F32, "Correct type");
+        ok(combined->numCols == SIZE && combined->numRows == SIZE, "Correct size");
+        int discrepant = 0;             // Number of discrepant pixels
+        for (int y = 0; y < SIZE; y++)
+        {
+            for (int x = 0; x < SIZE; x++) {
+                if (fabsf(combined->data.F32[y][x]) > 3.0 / sqrt(NUM)) {
+                    discrepant++;
+                }
+            }
+        }
+        ok(discrepant <= 30, "%d discrepant pixels", discrepant); // Should have 99.7% of 100x100 pixels OK
+
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+        psImageStats(stats, combined, NULL, 0);
+        ok(stats->sampleMean < 3.0 / sqrt(NUM * SIZE * SIZE), "Sample mean: %e", stats->sampleMean);
+        ok(fabs(stats->sampleStdev - 1.0 / sqrt(NUM)) < 1.0e-3, "Sample stdev: %e",
+           stats->sampleStdev);
+        psFree(stats);
+
+        skip_end();
+        psFree(combined);
+        psFree(images);
+    }
+
+}
Index: /branches/eam_branch_20071023/psModules/test/imcombine/tst_pmImageCombine.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/imcombine/tst_pmImageCombine.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/imcombine/tst_pmImageCombine.c	(revision 15358)
@@ -0,0 +1,365 @@
+/** @file tst_pmImageCombine.c
+ *
+ *  @brief Contains the tests for pmImageCombine.c:
+ *
+ *  test00: This code will test the various functions in the pmImageCombine.c file.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  XXX: Must verify the results internally.  Don't use stdout file.
+ *  XXX: Must test masks with pmRejectPixels()
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-03-04 01:01:34 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+#include "psTest.h"
+#include "pslib.h"
+#include "pmImageCombine.h"
+static int test00(void);
+testDescription tests[] = {
+                              {test00, 000, "pmCombineImages()", true, false},
+                              {NULL}
+                          };
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    return !runTestSuite(stderr, "Test Point Driver", tests, argc, argv);
+}
+
+psF32 genRanFloat(psF32 low,
+                  psF32 high)
+{
+    psF32 ran1 = (((psF32) (random() % 10000)) / 10000.0);
+    return(low + (ran1 * (high - low)));
+}
+
+psF32 genRanInt(psS32 low,
+                psS32 high)
+{
+    psF32 ran1 = (((psF32) (random() % 10000)) / 10000.0);
+    return((psS32) (low + (ran1 * (high - low))));
+}
+
+#define TST00_EXPANSION_FACTOR_X 1.0
+#define TST00_EXPANSION_FACTOR_Y 1.0
+#define TST00_OFFSET_X 0.0
+#define TST00_OFFSET_Y 0.0
+#define TST00_NUM_PIXELS 10
+#define TST00_MASK_VALUE 1
+#define TST00_NUM_ITERATIONS 4
+#define TST00_SIGMA_CLIP 1.0
+#define TST00_REJECTION_THRESHOLD 0.01
+#define TST00_GRADIENT_LIMIT 10.0
+/*******************************************************************************
+NOTE: This function returns FALSE if there were no errors.
+ ******************************************************************************/
+psBool testCombineImages(psS32 numRows,
+                         psS32 numCols,
+                         psS32 numImages)
+{
+    printf("Testing pmCombineImages(%d, %d, %d)\n", numRows, numCols, numImages);
+    bool testStatus = false;
+
+    psArray *images = psArrayAlloc(numImages);
+    psArray *errors = psArrayAlloc(numImages);
+    psArray *masks = psArrayAlloc(numImages);
+    images->n = images->nalloc;
+    errors->n = errors->nalloc;
+    masks->n = masks->nalloc;
+    for (psS32 i = 0 ; i < numImages ; i++) {
+        images->data[i] = (psPtr *) psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        errors->data[i] = (psPtr *) psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        masks->data[i] = (psPtr *) psImageAlloc(numCols, numRows, PS_TYPE_U8);
+        psImage *image = (psImage *) images->data[i];
+        psImage *error = (psImage *) errors->data[i];
+        psImage *mask = (psImage *) masks->data[i];
+        PS_IMAGE_SET_F32(image, 0.0);
+        PS_IMAGE_SET_F32(error, 1.0);
+        PS_IMAGE_SET_U8(mask, 0);
+
+        for (psS32 row = 0 ; row < numRows ; row++) {
+            for (psS32 col = 0 ; col < numCols ; col++) {
+                // Scale row/col to [-1.0:1.0]
+                psF32 rowScaled = ((psF32) (row - (numRows/2))) / ((psF32) (numRows/2));
+                psF32 colScaled = ((psF32) (col - (numCols/2))) / ((psF32) (numCols/2));
+                image->data.F32[row][col] = PS_SQR((2.0 - rowScaled) + (2.0 - colScaled)) + genRanFloat(0.0, 0.5);
+            }
+        }
+    }
+
+    //
+    // Same as above except the numImages is wrong
+    //
+    psArray *imagesLong = psArrayAlloc(numImages+1);
+    psArray *errorsLong = psArrayAlloc(numImages+1);
+    psArray *masksLong = psArrayAlloc(numImages+1);
+    imagesLong->n = imagesLong->nalloc;
+    errorsLong->n = errorsLong->nalloc;
+    masksLong->n = masksLong->nalloc;
+    for (psS32 i = 0 ; i < numImages+1 ; i++) {
+        imagesLong->data[i] = (psPtr *) psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        errorsLong->data[i] = (psPtr *) psImageAlloc(numCols, numRows, PS_TYPE_F32);
+        masksLong->data[i] = (psPtr *) psImageAlloc(numCols, numRows, PS_TYPE_U8);
+        psImage *image = (psImage *) imagesLong->data[i];
+        psImage *error = (psImage *) errorsLong->data[i];
+        psImage *mask = (psImage *) masksLong->data[i];
+        PS_IMAGE_SET_F32(image, 0.0);
+        PS_IMAGE_SET_F32(error, 1.0);
+        PS_IMAGE_SET_U8(mask, 0);
+
+        for (psS32 row = 0 ; row < numRows ; row++) {
+            for (psS32 col = 0 ; col < numCols ; col++) {
+                // Scale row/col to [-1.0:1.0]
+                psF32 rowScaled = ((psF32) (row - (numRows/2))) / ((psF32) (numRows/2));
+                psF32 colScaled = ((psF32) (col - (numCols/2))) / ((psF32) (numCols/2));
+                image->data.F32[row][col] = PS_SQR((2.0 - rowScaled) + (2.0 - colScaled)) + genRanFloat(0.0, 0.5);
+            }
+        }
+    }
+
+    //
+    // Same as above except the type is wrong
+    //
+    psArray *imagesBadType = psArrayAlloc(numImages);
+    psArray *errorsBadType = psArrayAlloc(numImages);
+    psArray *masksBadType = psArrayAlloc(numImages);
+    imagesBadType->n = imagesBadType->nalloc;
+    errorsBadType->n = errorsBadType->nalloc;
+    masksBadType->n = masksBadType->nalloc;
+    for (psS32 i = 0 ; i < numImages ; i++) {
+        imagesBadType->data[i] = (psPtr *) psImageAlloc(numCols, numRows, PS_TYPE_F64);
+        errorsBadType->data[i] = (psPtr *) psImageAlloc(numCols, numRows, PS_TYPE_F64);
+        masksBadType->data[i] = (psPtr *) psImageAlloc(numCols, numRows, PS_TYPE_S8);
+        psImage *image = (psImage *) imagesBadType->data[i];
+        psImage *error = (psImage *) errorsBadType->data[i];
+        psImage *mask = (psImage *) masksBadType    ->data[i];
+        PS_IMAGE_SET_F32(image, 0.0);
+        PS_IMAGE_SET_F32(error, 1.0);
+        PS_IMAGE_SET_U8(mask, 0);
+
+        for (psS32 row = 0 ; row < numRows ; row++) {
+            for (psS32 col = 0 ; col < numCols ; col++) {
+                // Scale row/col to [-1.0:1.0]
+                psF32 rowScaled = ((psF32) (row - (numRows/2))) / ((psF32) (numRows/2));
+                psF32 colScaled = ((psF32) (col - (numCols/2))) / ((psF32) (numCols/2));
+                image->data.F32[row][col] = PS_SQR((2.0 - rowScaled) + (2.0 - colScaled)) + genRanFloat(0.0, 0.5);
+            }
+        }
+    }
+
+    psPixels *pixels = psPixelsAlloc(TST00_NUM_PIXELS);
+    pixels->n = pixels->nalloc;
+    for (psS32 p = 0 ; p < TST00_NUM_PIXELS ; p++) {
+        psS32 col =  genRanInt(0, numCols);
+        psS32 row =  genRanInt(0, numRows);
+        pixels->data[p].x = (psF32) col;
+        pixels->data[p].y = (psF32) row;
+        psS32 im = genRanInt(0, numImages-1);
+        psImage *image = (psImage *) images->data[im];
+        image->data.F32[row][col] += 100.0;
+        printf("Generating a bad pixel in image (%d) at (%d, %d)\n", im, row, col);
+    }
+
+    psArray *questionablePixels = NULL;
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a NULL images.  Should generate error, return NULL.\n");
+    psImage *outImg = pmCombineImages(NULL, &questionablePixels, NULL, errors,
+                                      masks, TST00_MASK_VALUE, pixels, TST00_NUM_ITERATIONS,
+                                      TST00_SIGMA_CLIP, stats);
+    if (outImg != NULL) {
+        printf("TEST ERROR: pmCombineImages() returned a non-NULL psImage.\n");
+        psFree(outImg);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a long images.  Should generate error, return NULL.\n");
+    outImg = pmCombineImages(NULL, &questionablePixels, imagesLong, errors,
+                             masks, TST00_MASK_VALUE, pixels, TST00_NUM_ITERATIONS,
+                             TST00_SIGMA_CLIP, stats);
+    if (outImg != NULL) {
+        printf("TEST ERROR: pmCombineImages() returned a non-NULL psImage.\n");
+        psFree(outImg);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a bad type images.  Should generate error, return NULL.\n");
+    outImg = pmCombineImages(NULL, &questionablePixels, imagesBadType, errors,
+                             masks, TST00_MASK_VALUE, pixels, TST00_NUM_ITERATIONS,
+                             TST00_SIGMA_CLIP, stats);
+    if (outImg != NULL) {
+        printf("TEST ERROR: pmCombineImages() returned a non-NULL psImage.\n");
+        psFree(outImg);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a long errors.  Should generate error, return NULL.\n");
+    outImg = pmCombineImages(NULL, &questionablePixels, images, errorsLong,
+                             masks, TST00_MASK_VALUE, pixels, TST00_NUM_ITERATIONS,
+                             TST00_SIGMA_CLIP, stats);
+    if (outImg != NULL) {
+        printf("TEST ERROR: pmCombineImages() returned a non-NULL psImage.\n");
+        psFree(outImg);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a bad type errors.  Should generate error, return NULL.\n");
+    outImg = pmCombineImages(NULL, &questionablePixels, images, errorsBadType,
+                             masks, TST00_MASK_VALUE, pixels, TST00_NUM_ITERATIONS,
+                             TST00_SIGMA_CLIP, stats);
+    if (outImg != NULL) {
+        printf("TEST ERROR: pmCombineImages() returned a non-NULL psImage.\n");
+        psFree(outImg);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a long masks.  Should generate error, return NULL.\n");
+    outImg = pmCombineImages(NULL, &questionablePixels, images, errors,
+                             masksLong, TST00_MASK_VALUE, pixels, TST00_NUM_ITERATIONS,
+                             TST00_SIGMA_CLIP, stats);
+    if (outImg != NULL) {
+        printf("TEST ERROR: pmCombineImages() returned a non-NULL psImage.\n");
+        psFree(outImg);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a bad type masks.  Should generate error, return NULL.\n");
+    outImg = pmCombineImages(NULL, &questionablePixels, images, errors,
+                             masksBadType, TST00_MASK_VALUE, pixels, TST00_NUM_ITERATIONS,
+                             TST00_SIGMA_CLIP, stats);
+    if (outImg != NULL) {
+        printf("TEST ERROR: pmCombineImages() returned a non-NULL psImage.\n");
+        psFree(outImg);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a NULL stats.  Should generate error, return NULL.\n");
+    outImg = pmCombineImages(NULL, &questionablePixels, images, errors,
+                             masks, TST00_MASK_VALUE, pixels, TST00_NUM_ITERATIONS,
+                             TST00_SIGMA_CLIP, NULL);
+    if (outImg != NULL) {
+        printf("TEST ERROR: pmCombineImages() returned a non-NULL psImage.\n");
+        psFree(outImg);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with acceptable data.  Should generate a psImage.\n");
+    outImg = pmCombineImages(NULL, &questionablePixels, images, errors,
+                             masks, TST00_MASK_VALUE, pixels, TST00_NUM_ITERATIONS,
+                             TST00_SIGMA_CLIP, stats);
+    if (outImg == NULL) {
+        printf("TEST ERROR: pmCombineImages() returned a NULL psImage.\n");
+        testStatus = true;
+    }
+    if (0) {
+        for (psS32 p = 0 ; p < TST00_NUM_PIXELS ; p++) {
+            psS32 col = (psS32) (pixels->data[p]).x;
+            psS32 row = (psS32) (pixels->data[p]).y;
+            printf("------------------------------------------\n");
+            printf("Pixel (%d, %d) in combined image is %f\n", row, col, outImg->data.F32[row][col]);
+            for (psS32 i = 0 ; i < numImages ; i++) {
+                psImage *image = (psImage *) images->data[i];
+                printf("Pixel (after combine) (%d, %d) in image (%d) is %f\n", row, col, i, image->data.F32[row][col]);
+            }
+        }
+    }
+
+    if (questionablePixels->n != numImages) {
+        printf("TEST ERROR: pmCombineImages(): questionablePixels->n was %ld, should have been %d\n",
+               questionablePixels->n, numImages);
+        testStatus = true;
+    } else {
+        // XXX: We should internally verify this with the pixels list, not merely use the stdout.
+        for (psS32 i = 0 ; i < questionablePixels->n ; i++) {
+            psPixels *myPixels = (psPixels *) questionablePixels->data[i];
+            for (psS32 p = 0 ; p < myPixels->n ; p++) {
+                printf("Image %d, questionable pixel %d is (%f %f)\n",
+                       i, p, myPixels->data[p].y, myPixels->data[p].x);
+            }
+        }
+    }
+
+    psArray *expandTransforms = psArrayAlloc(numImages);
+    psArray *contractTransforms = psArrayAlloc(numImages);
+    expandTransforms->n = expandTransforms->nalloc;
+    contractTransforms->n = contractTransforms->nalloc;
+    for (psS32 im = 0 ; im < numImages ; im++) {
+        psPlaneTransform *ptExpand = psPlaneTransformAlloc(2, 2);
+        ptExpand->x->coeff[0][0] = TST00_OFFSET_X;
+        ptExpand->x->coeff[1][0] = TST00_EXPANSION_FACTOR_X;
+        ptExpand->y->coeff[0][0] = TST00_OFFSET_Y;
+        ptExpand->y->coeff[0][1] = TST00_EXPANSION_FACTOR_Y;
+        expandTransforms->data[im] = (psPtr *) ptExpand;
+
+        psPlaneTransform *ptContract = psPlaneTransformAlloc(2, 2);
+        ptContract->x->coeff[0][0] = -TST00_OFFSET_X;
+        ptContract->x->coeff[1][0] = 1.0 / TST00_EXPANSION_FACTOR_X;
+        ptContract->y->coeff[0][0] = -TST00_OFFSET_Y;
+        ptContract->y->coeff[0][1] = 1.0 / TST00_EXPANSION_FACTOR_Y;
+        contractTransforms->data[im] = (psPtr *) ptContract;
+    }
+
+    //-------------------------------------------------------------------------
+    //
+    // XXX: psRejectPixels() has known bugs.  Specifically, in the psImageTransform() call.
+    // We exclude this from our tests.
+    //
+    printf("\n\n\nCalling pmRejectPixels() with acceptable data.  Should generate a psArray.\n");
+    psArray *pixelRejects = pmRejectPixels(images, NULL, questionablePixels, expandTransforms,
+                                           contractTransforms, TST00_REJECTION_THRESHOLD,
+                                           TST00_GRADIENT_LIMIT);
+    if (pixelRejects == NULL) {
+        printf("TEST ERROR: pmRejectPixels() returned a NULL psArray.\n");
+        testStatus = true;
+    } else {
+        // XXX: We should internally verify this with the pixels list, not merely use the stdout.
+        for (psS32 i = 0 ; i < pixelRejects->n ; i++) {
+            psPixels *myPixels = (psPixels *) pixelRejects->data[i];
+            printf("tst_pmImageCombine.c: Image %d had %ld rejects.\n", i, myPixels->n);
+
+            for (psS32 p = 0 ; p < myPixels->n ; p++) {
+                printf("Image %d, rejected pixel %d is (%f %f)\n", i, p,
+                       myPixels->data[p].y, myPixels->data[p].x);
+            }
+        }
+        psFree(pixelRejects);
+    }
+
+    psFree(images);
+    psFree(errors);
+    psFree(masks);
+    psFree(imagesLong);
+    psFree(errorsLong);
+    psFree(masksLong);
+    psFree(imagesBadType);
+    psFree(errorsBadType);
+    psFree(masksBadType);
+    psFree(pixels);
+    psFree(stats);
+
+    return(testStatus);
+}
+
+/*******************************************************************************
+NOTE: This function returns TRUE if there were no errors.
+ ******************************************************************************/
+int test00( void )
+{
+    bool testStatus = false;
+
+    testStatus|= testCombineImages(10, 10, 5);
+
+    return(!testStatus);
+}
Index: /branches/eam_branch_20071023/psModules/test/imcombine/tst_pmImageSubtract.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/imcombine/tst_pmImageSubtract.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/imcombine/tst_pmImageSubtract.c	(revision 15358)
@@ -0,0 +1,847 @@
+/** @file tst_pmImageSubtract.c
+ *
+ *  @brief Contains the tests for pmImageSubtract.c:
+ *
+ *  test00: This code will test the various functions in pmObjects.c
+ *
+ *  @author GLG, MHPCC
+ *
+ *  XXX: Most test simply ensure that the functions can be called with allowable
+ *  data.  More work need to be done to verify the results.
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-05-25 22:02:46 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+#include "psTest.h"
+#include "pslib.h"
+#include "pmImageSubtract.h"
+#define ERROR_TOLERANCE 1.0
+static int test00(void);
+static int test01(void);
+static int test02(void);
+static int test03(void);
+testDescription tests[] = {
+                              {test00, 000, "pmSubtractionKernelsAllocPOIS()", true, false},
+                              {test01, 000, "pmSubtractionKernelsAllocISIS()", true, false},
+                              {test02, 000, "pmSubtractionFindStamps()", true, false},
+                              {test03, 000, "pmSubtractionCalculateEquation()", true, false},
+                              {NULL}
+                          };
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    return !runTestSuite(stderr, "Test Point Driver", tests, argc, argv);
+}
+
+
+/*******************************************************************************
+NOTE: This function returns FALSE if there were no errors.
+ 
+XXX: Call with various unallowable input parameters.
+ 
+XXX: Untested: we don't loop through the (u, v, xOrder, yOrder) psVectors and
+ensure that each value is set correctly.
+ ******************************************************************************/
+psBool testPOISAlloc(psS32 size,
+                     psS32 SpatialOrder)
+{
+    printf("Testing pmSubtractionKernelsAllocPOIS(%d, %d)\n", size, SpatialOrder);
+
+    bool testStatus = false;
+    psS32 nBasisFunctions = (2 * size + 1) * (2 * size + 1) * (SpatialOrder + 1) * (SpatialOrder + 2) / 2;
+
+    psSubtractionKernels *kernels = pmSubtractionKernelsAllocPOIS(size, SpatialOrder);
+    if (kernels == NULL) {
+        printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() returned a NULL psSubtractionKernels.\n");
+        testStatus = true;
+    } else {
+        if (kernels->type != PM_SUBTRACTION_KERNEL_POIS) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated the wrong kernels->type.\n");
+            testStatus = true;
+        }
+
+        if ((kernels->u == NULL) ||
+                (kernels->u->n != nBasisFunctions)) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated a NULL ->u member or\n");
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated a incorrect length ->u member.\n");
+            testStatus = true;
+        }
+
+        if ((kernels->v == NULL) ||
+                (kernels->v->n != nBasisFunctions)) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated a NULL ->v member or\n");
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated a incorrect length ->v member.\n");
+            testStatus = true;
+        }
+
+        if (kernels->sigma != NULL) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated a non-NULL ->sigma member.\n");
+            testStatus = true;
+        }
+
+        if ((kernels->xOrder == NULL) ||
+                (kernels->xOrder->n != nBasisFunctions)) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated a NULL ->xOrder member or\n");
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated a incorrect length ->xOrder member.\n");
+            testStatus = true;
+        }
+
+        if ((kernels->yOrder == NULL) ||
+                (kernels->yOrder->n != nBasisFunctions)) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated a NULL ->yOrder member or\n");
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated a incorrect length ->yOrder member.\n");
+            testStatus = true;
+        }
+
+        if (kernels->subIndex != 0) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated a non-zero ->subIndex member (%d).\n", kernels->subIndex);
+            testStatus = true;
+        }
+
+        psS32 i = kernels->subIndex;
+        if ((kernels->u->data.F32[i] != 0) ||
+                (kernels->v->data.F32[i] != 0)) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS(): the ->subIndex member points to a kernel with (%f, %f) (u, v) basis function.\n",
+                   kernels->u->data.F32[i], kernels->v->data.F32[i]);
+            testStatus = true;
+        }
+
+        if (kernels->preCalc != NULL) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated a non-NULL ->preCalc member.\n");
+            testStatus = true;
+        }
+
+        if (kernels->size != size) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated an incorrect ->size member (%d).\n", kernels->size);
+            testStatus = true;
+        }
+
+        if (kernels->spatialOrder != SpatialOrder) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocPOIS() generated an incorrect ->spatialOrder member (%d).\n", kernels->spatialOrder);
+            testStatus = true;
+        }
+    }
+    psFree(kernels);
+    return(testStatus);
+}
+
+/*******************************************************************************
+NOTE: This function returns TRUE if there were no errors.
+ ******************************************************************************/
+int test00( void )
+{
+    bool testStatus = false;
+
+    testStatus|= testPOISAlloc(1, 1);
+    testStatus|= testPOISAlloc(2, 3);
+    testStatus|= testPOISAlloc(3, 4);
+
+    return(!testStatus);
+}
+
+/*******************************************************************************
+NOTE: This function returns FALSE if there were no errors.
+ 
+XXX: Call with various unallowable input parameters.
+ 
+XXX: Untested: we don't loop through the (u, v, xOrder, yOrder) psVectors and
+ensure that each value is set correctly.  We don't ensure that he preCalc
+psImages are set correctly.
+ ******************************************************************************/
+psBool testISISAlloc(psS32 sigmaLength,
+                     psS32 orderLength,
+                     psS32 size,
+                     psS32 SpatialOrder)
+{
+    printf("Testing pmSubtractionKernelsAllocISIS(%d, %d, %d, %d)\n",
+           sigmaLength, orderLength, size, SpatialOrder);
+
+    psVector *sigmas = psVectorAlloc(sigmaLength, PS_TYPE_F32);
+    sigmas->n = sigmas->nalloc;
+    for (psS32 i = 0 ; i < sigmas->n ; i++) {
+        sigmas->data.F32[i] = 1.0 + (psF32) i;
+    }
+    psVector *orders = psVectorAlloc(orderLength, PS_TYPE_S32);
+    orders->n = orders->nalloc;
+    for (psS32 i = 0 ; i < orders->n ; i++) {
+        orders->data.S32[i] = i + 2;
+    }
+
+    bool testStatus = false;
+    psS32 numSigmas = sigmas->n;
+    psS32 nBasisFunctions = 0;
+    for (psS32 s = 0 ; s < numSigmas ; s++) {
+        for (psS32 o = 0 ; o < orders->n ; o++) {
+            nBasisFunctions+= ((orders->data.S32[o] + 1) * (orders->data.S32[o] + 2) / 2);
+        }
+    }
+    nBasisFunctions*= ((SpatialOrder + 1) * (SpatialOrder + 2) / 2);
+
+    psSubtractionKernels *kernels = pmSubtractionKernelsAllocISIS(sigmas, orders, size, SpatialOrder);
+    if (kernels == NULL) {
+        printf("TEST ERROR: pmSubtractionKernelsAllocISIS() returned a NULL psSubtractionKernels.\n");
+        testStatus = true;
+    } else {
+        if (kernels->type != PM_SUBTRACTION_KERNEL_ISIS) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated the wrong kernels->type.\n");
+            testStatus = true;
+        }
+
+        if ((kernels->u == NULL) ||
+                (kernels->u->n != nBasisFunctions)) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a NULL ->u member or\n");
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a incorrect length ->u member.\n");
+            testStatus = true;
+        }
+
+        if ((kernels->v == NULL) ||
+                (kernels->v->n != nBasisFunctions)) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a NULL ->v member or\n");
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a incorrect length ->v member.\n");
+            testStatus = true;
+        }
+
+        if ((kernels->sigma == NULL) ||
+                (kernels->sigma->n != nBasisFunctions)) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a NULL ->sigma member or\n");
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a incorrect length ->sigma member.\n");
+            testStatus = true;
+        }
+
+        if ((kernels->xOrder == NULL) ||
+                (kernels->xOrder->n != nBasisFunctions)) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a NULL ->xOrder member or\n");
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a incorrect length ->xOrder member.\n");
+            testStatus = true;
+        }
+
+        if ((kernels->yOrder == NULL) ||
+                (kernels->yOrder->n != nBasisFunctions)) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a NULL ->yOrder member or\n");
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a incorrect length ->yOrder member.\n");
+            testStatus = true;
+        }
+
+        if (kernels->subIndex != 0) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a non-zero ->subIndex member (%d).\n", kernels->subIndex);
+            testStatus = true;
+        }
+
+        //
+        // Ensure that kernels->subIndex points to the correct kernel.
+        //
+        psS32 i = kernels->subIndex;
+        if ((kernels->u->data.F32[i] != 0.0) ||
+                (kernels->v->data.F32[i] != 0.0) ||
+                (kernels->xOrder->data.F32[i] != 0.0) ||
+                (kernels->yOrder->data.F32[i] != 0.0) ||
+                (kernels->sigma->data.F32[i] != sigmas->data.F32[0])) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS(): the ->subIndex member points to the wrong kernel.\n");
+            printf("TEST ERROR: (u, v, xOrder, yOrder, sigma) is (%f, %f, %f, %f, %f).\n",
+                   kernels->u->data.F32[i], kernels->v->data.F32[i],
+                   kernels->xOrder->data.F32[i], kernels->yOrder->data.F32[i],
+                   kernels->sigma->data.F32[i]);
+            testStatus = true;
+        }
+
+        //
+        // Ensure that the preCalc images are allocated correctly.
+        //
+        if ((kernels->preCalc == NULL) ||
+                (kernels->preCalc->n != nBasisFunctions)) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a NULL ->preCalc member or\n");
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated a incorrect length ->preCalc member.\n");
+            testStatus = true;
+        } else {
+            for (psS32 k = 0 ; k < kernels->u->n ; k++) {
+                psImage *kerImg = (psImage *) kernels->preCalc->data[k];
+                if (kerImg == NULL) {
+                    printf("TEST ERROR: the %d-th kernel preCalc image is NULL.\n", k);
+                    testStatus = true;
+                } else {
+                    if (kerImg->type.type != PS_TYPE_F32) {
+                        printf("TEST ERROR: preCalc image %d had ioncorrect type.\n", k);
+                        testStatus = true;
+                    }
+                    if ((kerImg->numRows != (1 + (2 * size))) ||
+                            (kerImg->numCols != (1 + (2 * size)))) {
+                        printf("TEST ERROR: preCalc image %d had incorrect size (%d, %d).\n", k,
+                               kerImg->numRows, kerImg->numCols);
+                        testStatus = true;
+                    }
+                }
+            }
+        }
+
+        if (kernels->size != size) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated an incorrect ->size member (%d).\n", kernels->size);
+            testStatus = true;
+        }
+
+        if (kernels->spatialOrder != SpatialOrder) {
+            printf("TEST ERROR: pmSubtractionKernelsAllocISIS() generated an incorrect ->spatialOrder member (%d).\n", kernels->spatialOrder);
+            testStatus = true;
+        }
+    }
+    psFree(sigmas);
+
+    psFree(kernels->u);
+    psFree(kernels->v);
+    psFree(kernels->sigma);
+    psFree(kernels->xOrder);
+    psFree(kernels->yOrder);
+    psFree(kernels->preCalc);
+    psFree(kernels);
+    psFree(orders);
+    return(testStatus);
+}
+
+/*******************************************************************************
+NOTE: This function returns TRUE if there were no errors.
+ ******************************************************************************/
+int test01( void )
+{
+    bool testStatus = false;
+
+    /*
+        testStatus|= testISISAlloc(1, 1, 1, 1);
+        testStatus|= testISISAlloc(2, 2, 2, 2);
+        testStatus|= testISISAlloc(2, 3, 4, 5);
+        testStatus|= testISISAlloc(3, 4, 5, 6);
+    */
+
+    return(!testStatus);
+}
+
+
+/*******************************************************************************
+NOTE: This function returns FALSE if there were no errors.
+ 
+XXX: Can we test to ensure that no stamps overlap?
+ 
+XXX: Test stamp alloc/dealloc functions.
+ ******************************************************************************/
+#define TST02_THRESHOLD 3.0
+#define TST02_MASK_VAL 1
+psBool testFindStamps(psS32 numCols,
+                      psS32 numRows,
+                      psS32 xNum,
+                      psS32 yNum,
+                      psS32 border)
+{
+    printf("Testing pmSubtractionFindStamps(%d, %d, %d, %d, %d)\n",
+           numCols, numRows, xNum, yNum, border);
+    bool testStatus = false;
+
+    // Create a test image and set a single pixel in the center of each stamp.
+    psImage *tstImg = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    int numStamps = 0;
+    PS_IMAGE_SET_F32(tstImg, 0.0);
+    for (psS32 j = 0; j < yNum; j++) {
+        for (psS32 i = 0; i < xNum; i++) {
+            psS32 yMin = border + j * (numRows - 2.0 * border) / yNum;
+            psS32 yMax = PS_MIN(numRows-1, (border + (j + 1) * (numRows - 2.0 * border) / yNum) - 1);
+            psS32 xMin = border + i * (numCols - 2.0 * border) / xNum;
+            psS32 xMax = PS_MIN(numCols-1, (border + (i + 1) * (numCols - 2.0 * border) / xNum) - 1);
+
+            tstImg->data.F32[(yMax+yMin)/2][(xMax+xMin)/2] = TST02_THRESHOLD + (psF32) (i + j);
+            numStamps++;
+        }
+    }
+    psImage *tmpMask= psImageAlloc(numCols, numRows, PS_TYPE_U8);
+    PS_IMAGE_SET_U8(tstImg, 0);
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a NULL psImage.  Should generate error, return NULL.\n");
+    psArray *stamps = pmSubtractionFindStamps(NULL, NULL, tmpMask, TST02_MASK_VAL,
+                      TST02_THRESHOLD, xNum, yNum,
+                      border);
+    if (stamps != NULL) {
+        printf("TEST ERROR: pmSubtractionFindStamps returned a non-NULL psArray.\n");
+        psFree(stamps);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a non-positive xNum.  Should generate error, return NULL.\n");
+    stamps = pmSubtractionFindStamps(NULL, tstImg, tmpMask, TST02_MASK_VAL,
+                                     TST02_THRESHOLD, 0, yNum,
+                                     border);
+    if (stamps != NULL) {
+        printf("TEST ERROR: pmSubtractionFindStamps returned a non-NULL psArray.\n");
+        psFree(stamps);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a non-positive yNum.  Should generate error, return NULL.\n");
+    stamps = pmSubtractionFindStamps(NULL, tstImg, tmpMask, TST02_MASK_VAL,
+                                     TST02_THRESHOLD, xNum, 0,
+                                     border);
+    if (stamps != NULL) {
+        printf("TEST ERROR: pmSubtractionFindStamps returned a non-NULL psArray.\n");
+        psFree(stamps);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with a non-positive border.  Should generate error, return NULL.\n");
+    stamps = pmSubtractionFindStamps(NULL, tstImg, tmpMask, TST02_MASK_VAL,
+                                     TST02_THRESHOLD, xNum, yNum,
+                                     0);
+    if (stamps != NULL) {
+        printf("TEST ERROR: pmSubtractionFindStamps returned a non-NULL psArray.\n");
+        psFree(stamps);
+        testStatus = true;
+    }
+
+    //-------------------------------------------------------------------------
+    printf("Calling with acceptable input parameters, non-NULL mask.\n");
+    stamps = pmSubtractionFindStamps(NULL, tstImg, tmpMask, TST02_MASK_VAL,
+                                     TST02_THRESHOLD, xNum, yNum,
+                                     border);
+    if (stamps == NULL) {
+        printf("TEST ERROR: pmSubtractionFindStamps returned a non-NULL psArray.\n");
+        testStatus = true;
+    } else {
+        if (stamps->n != numStamps) {
+            printf("TEST ERROR: %ld stamps were found, %d were expected.\n",
+                   stamps->n, numStamps);
+            testStatus = true;
+        }
+        for (psS32 s = 0 ; s < stamps->n ; s++) {
+            pmStamp *stamp = (pmStamp *) stamps->data[s];
+            psS32 row = stamp->y;
+            psS32 col = stamp->x;
+            // printf("Stamp %d at (%d, %d) has a value of %f\n", s, row, col, tstImg->data.F32[row][col]);
+            if (tstImg->data.F32[row][col] < TST02_THRESHOLD) {
+                if (stamp->status != PM_STAMP_NONE) {
+                    printf("TEST ERROR: stamp %d had peak value %f (below theshold) and the status was not set to PM_STAMP_NONE.\n",
+                           s, tstImg->data.F32[row][col]);
+                    testStatus = true;
+                }
+            } else {
+                if (stamp->status != PM_STAMP_RECALC) {
+                    printf("TEST ERROR: stamp %d had peak value %f (above theshold) and the status was not set to PM_STAMP_RECALC.\n",
+                           s, tstImg->data.F32[row][col]);
+                    testStatus = true;
+                }
+            }
+        }
+    }
+    psFree(stamps);
+
+    //-------------------------------------------------------------------------
+    printf("Calling with acceptable input parameters, NULL mask.\n");
+    stamps = pmSubtractionFindStamps(NULL, tstImg, NULL, TST02_MASK_VAL,
+                                     TST02_THRESHOLD, xNum, yNum,
+                                     border);
+    if (stamps == NULL) {
+        printf("TEST ERROR: pmSubtractionFindStamps returned a non-NULL psArray.\n");
+        testStatus = true;
+    } else {
+        if (stamps->n != numStamps) {
+            printf("TEST ERROR: %ld stamps were found, %d were expected.\n",
+                   stamps->n, numStamps);
+            testStatus = true;
+        }
+        for (psS32 s = 0 ; s < stamps->n ; s++) {
+            pmStamp *stamp = (pmStamp *) stamps->data[s];
+            psS32 row = stamp->y;
+            psS32 col = stamp->x;
+            //printf("Stamp %d at (%d, %d) has a value of %f\n", s, row, col, tstImg->data.F32[row][col]);
+            if (tstImg->data.F32[row][col] < TST02_THRESHOLD) {
+                if (stamp->status != PM_STAMP_NONE) {
+                    printf("TEST ERROR: stamp %d had peak value %f (below theshold) and the status was not set to PM_STAMP_NONE.\n",
+                           s, tstImg->data.F32[row][col]);
+                    testStatus = true;
+                }
+            } else {
+                if (stamp->status != PM_STAMP_RECALC) {
+                    printf("TEST ERROR: stamp %d had peak value %f (above theshold) and the status was not set to PM_STAMP_RECALC.\n",
+                           s, tstImg->data.F32[row][col]);
+                    testStatus = true;
+                }
+            }
+        }
+    }
+
+    psFree(tstImg);
+    psFree(tmpMask);
+    psFree(stamps);
+
+    return(testStatus);
+}
+
+
+
+/*******************************************************************************
+NOTE: This function returns TRUE if there were no errors.
+ ******************************************************************************/
+int test02( void )
+{
+    bool testStatus = false;
+
+    testStatus|= testFindStamps(100, 100, 2, 2, 2);
+    testStatus|= testFindStamps(100, 100, 10, 10, 2);
+
+    return(!testStatus);
+}
+
+
+psF32 genRanFloat(psF32 low,
+                  psF32 high)
+{
+    psF32 ran1 = (((psF32) (random() % 10000)) / 10000.0);
+    return(low + (ran1 * (high - low)));
+}
+
+//
+// XXX: POIS kernels are producing NANs if the image size is 20 or less.
+//
+// XXX: ISIS kernels are producing NANS if the TST03_ORDER_LENGTH is 2 or larger.
+//
+#define TST03_THRESHOLD  3.0
+#define TST03_MASK_VAL  1
+#define TST03_KERNEL_SIZE 2
+#define TST03_SPATIAL_ORDER 2
+#define TST03_ORDER_LENGTH 1
+#define TST03_SIGMA_LENGTH 1
+#define TST03_PSF_MAX  10.0
+#define TST03_BG 0.0
+#define TST03_IMAGE_SIZE 25
+#define TST03_NUM_COLS  TST03_IMAGE_SIZE
+#define TST03_NUM_ROWS  TST03_IMAGE_SIZE
+#define TST03_NUM_STAMPS 2
+#define TST03_NUM_STAMPS_COLS TST03_NUM_STAMPS
+#define TST03_NUM_STAMPS_ROWS TST03_NUM_STAMPS
+#define TST03_BORDER  TST03_KERNEL_SIZE
+//#define TST03_FOOTPRINT (((TST03_IMAGE_SIZE - (2 * TST03_BORDER)) / TST03_NUM_STAMPS) - TST03_KERNEL_SIZE)
+#define TST03_FOOTPRINT  4
+#define TST03_PSF_WIDTH  (TST03_FOOTPRINT/2 - 1)
+
+/*******************************************************************************
+This routine generates an object in the center of the stamp defined by the
+(xMin, xMax) and (yMin, yMax) boundary.
+ ******************************************************************************/
+psBool genObject(psImage *tstImg,
+                 psImage *refImg,
+                 psS32 xMin,
+                 psS32 xMax,
+                 psS32 yMin,
+                 psS32 yMax)
+{
+    if (((1 + 2 * TST03_PSF_WIDTH) > (xMax - xMin)) ||
+            ((1 + 2 * TST03_PSF_WIDTH) > (yMax - yMin))) {
+        printf("INCORRECT TEST CONFIGURATION: TST03_PSF_WIDTH is too big.\n");
+        printf("TST03_PSF_WIDTH is %d: (xMin - xMax) is %d\n", TST03_PSF_WIDTH, (xMax - xMin));
+        return(FALSE);
+    }
+    //
+    // This code basically creates a peak at the center of the stamp with
+    // a height of TST03_PSF_MAX
+    //
+    psS32 xCenter = (xMax + xMin) / 2;
+    psS32 yCenter = (yMax + yMin) / 2;
+    psF32 subImageWidth = 1.0 + sqrtf(PS_SQR(((psF32) ((yMax-yMin)/2))) + PS_SQR(((psF32) ((xMax-xMin)/2))));
+    for (psS32 y = yMin ; y <= yMax ; y++) {
+        for (psS32 x = xMin ; x <= xMax ; x++) {
+            psF32 dist = sqrtf(PS_SQR((psF32) (y - yCenter)) + PS_SQR((psF32) (x - xCenter)));
+            psF32 pixel = TST03_PSF_MAX * PS_SQR(((psF32) (subImageWidth - dist)) / ((psF32) subImageWidth));
+            if (pixel < 0.0) {
+                pixel = 0.0;
+            }
+            refImg->data.F32[y][x] = pixel + TST03_BG;
+            tstImg->data.F32[y][x] = pixel + TST03_BG;
+            // Add some noise for the test image.
+            tstImg->data.F32[y][x]+= genRanFloat(0.0, 1.0);
+        }
+    }
+
+    return(TRUE);
+}
+
+/*******************************************************************************
+NOTE: This function returns FALSE if there were no errors.
+ 
+XXX: We should use a larger variety of input parameter configurations.
+ 
+I test the following functions here (since they linearly rely on a set of data
+structures that the previoues ones generate):
+    pmSubtractionCalculateEquation()
+    pmSubtractionSolveEquation()
+    pmSubtractionRejectStamps()
+    pmSubtractionKernelImage()
+ ******************************************************************************/
+psBool testSubCalcEqu(psS32 numCols,
+                      psS32 numRows,
+                      psS32 xNum,
+                      psS32 yNum,
+                      psS32 border,
+                      pmSubtractionKernelsType KernelType)
+{
+    printf("Testing pmSubtractionCalculateEquation(): \n");
+    printf("    image size is (%d, %d)\n", numRows, numCols);
+    printf("    num stamps is (%d, %d).  Border is %d\n", yNum, xNum, border);
+    if (KernelType == PM_SUBTRACTION_KERNEL_POIS) {
+        printf("   kernel type is PM_SUBTRACTION_KERNEL_POIS.\n");
+    } else if (KernelType == PM_SUBTRACTION_KERNEL_ISIS) {
+        printf("   kernel type is PM_SUBTRACTION_KERNEL_ISIS.\n");
+    }
+    bool testStatus = false;
+
+    // Create a test image and set a single pixel in the center of each stamp.
+    psImage *tstImg = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *refImg = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psImage *maskImg = psImageAlloc(numCols, numRows, PS_TYPE_U8);
+    PS_IMAGE_SET_F32(tstImg, 0.0);
+    PS_IMAGE_SET_F32(maskImg, 0);
+    for (psS32 j = 0; j < yNum; j++) {
+        for (psS32 i = 0; i < xNum; i++) {
+            psS32 yMin = border + j * (numRows - 2.0 * border) / yNum;
+            psS32 yMax = PS_MIN(numRows-1, (border + (j + 1) * (numRows - 2.0 * border) / yNum) - 1);
+            psS32 xMin = border + i * (numCols - 2.0 * border) / xNum;
+            psS32 xMax = PS_MIN(numCols-1, (border + (i + 1) * (numCols - 2.0 * border) / xNum) - 1);
+
+            genObject(tstImg, refImg, xMin, xMax, yMin, yMax);
+        }
+    }
+    printf("Generating stamps...\n");
+    psArray *stamps = pmSubtractionFindStamps(NULL, tstImg, NULL, TST03_MASK_VAL,
+                      TST03_THRESHOLD, xNum, yNum,
+                      border);
+
+    //
+    // PsVectors sigmas and orders are for ISIS kernels only.
+    //
+    psVector *sigmas = psVectorAlloc(TST03_SIGMA_LENGTH, PS_TYPE_F32);
+    sigmas->n = sigmas->nalloc;
+    for (psS32 i = 0 ; i < sigmas->n ; i++) {
+        sigmas->data.F32[i] = 1.0 + (psF32) i;
+    }
+    psVector *orders = psVectorAlloc(TST03_ORDER_LENGTH, PS_TYPE_S32);
+    orders->n = orders->nalloc;
+    for (psS32 i = 0 ; i < orders->n ; i++) {
+        orders->data.S32[i] = i + 2;
+    }
+
+    //
+    // Create the subtraction kernels
+    //
+    printf("Generating kernel basis functions...\n");
+    psSubtractionKernels *myKernels = NULL;
+    if (KernelType == PM_SUBTRACTION_KERNEL_POIS) {
+        myKernels = pmSubtractionKernelsAllocPOIS(TST03_KERNEL_SIZE, TST03_SPATIAL_ORDER);
+    } else if (KernelType == PM_SUBTRACTION_KERNEL_ISIS) {
+        myKernels = pmSubtractionKernelsAllocISIS(sigmas, orders, TST03_KERNEL_SIZE, TST03_SPATIAL_ORDER);
+    }
+
+    if ((stamps == NULL) ||
+            (myKernels == NULL)) {
+        printf("TEST ERROR: stamps or myKernels is NULL.\n");
+        testStatus = true;
+    } else {
+        //-------------------------------------------------------------------------
+        printf("Calling with a NULL psArray stamps.  Should generate error, return FALSE.\n");
+        psBool rc = pmSubtractionCalculateEquation(NULL, refImg, tstImg, myKernels, TST03_FOOTPRINT);
+        if (rc == TRUE) {
+            printf("TEST ERROR: pmSubtractionCalculateEquation() returned TRUE.\n");
+            testStatus = true;
+        }
+
+        //-------------------------------------------------------------------------
+        printf("Calling with a NULL reference images.  Should generate error, return FALSE.\n");
+        rc = pmSubtractionCalculateEquation(stamps, NULL, tstImg, myKernels, TST03_FOOTPRINT);
+        if (rc == TRUE) {
+            printf("TEST ERROR: pmSubtractionCalculateEquation() returned TRUE.\n");
+            testStatus = true;
+        }
+
+        //-------------------------------------------------------------------------
+        printf("Calling with a NULL input images.  Should generate error, return FALSE.\n");
+        rc = pmSubtractionCalculateEquation(stamps, refImg, NULL, myKernels, TST03_FOOTPRINT);
+        if (rc == TRUE) {
+            printf("TEST ERROR: pmSubtractionCalculateEquation() returned TRUE.\n");
+            testStatus = true;
+        }
+
+        //-------------------------------------------------------------------------
+        printf("Calling with a NULL kernel basis functions.  Should generate error, return FALSE.\n");
+        rc = pmSubtractionCalculateEquation(stamps, refImg, tstImg, NULL, TST03_FOOTPRINT);
+        if (rc == TRUE) {
+            printf("TEST ERROR: pmSubtractionCalculateEquation() returned TRUE.\n");
+            testStatus = true;
+        }
+
+        //-------------------------------------------------------------------------
+        printf("Calling with acceptable input parameters.  Should return TRUE.\n");
+
+        rc = pmSubtractionCalculateEquation(stamps, refImg, tstImg, myKernels, TST03_FOOTPRINT);
+        if (rc != TRUE) {
+            printf("TEST ERROR: pmSubtractionCalculateEquation() returned FALSE.\n");
+            testStatus = true;
+        } else {
+
+            if (0) {
+                for (psS32 s = 0 ; s < stamps->n ; s++) {
+                    printf("********************************* Stamp %d *********************************\n", s);
+                    pmStamp *stamp = (pmStamp *) stamps->data[s];
+                    if (stamp->vector != NULL) {
+                        PS_VECTOR_PRINT_F64(stamp->vector);
+                    }
+                    if (stamp->matrix != NULL) {
+                        printf("Stamp matrix size is (%d, %d)\n", stamp->matrix->numRows, stamp->matrix->numCols);
+                        PS_IMAGE_PRINT_F64(stamp->matrix);
+                    }
+                }
+            }
+
+
+            //-------------------------------------------------------------------------
+            printf("Calling pmSubtractionSolveEquation() with a NULL stamp argument.  Should generate error, return FALSE.\n");
+            psVector *solution = pmSubtractionSolveEquation(NULL, NULL);
+            if (solution != NULL) {
+                printf("TEST ERROR: pmSubtractionSolveEquation() returned non-NULL.\n");
+                testStatus = true;
+            }
+
+            //-------------------------------------------------------------------------
+            printf("Calling pmSubtractionSolveEquation() with acceptable input parameters.  Should return non-NULL.\n");
+            solution = pmSubtractionSolveEquation(NULL, stamps);
+            if (solution == NULL) {
+                printf("TEST ERROR: pmSubtractionSolveEquation() returned NULL.\n");
+                testStatus = true;
+            } else {
+                printf("The solution vector is:\n");
+                for (psS32 i = 0 ; i < solution->n ; i++) {
+                    printf("(%.2f) ", solution->data.F64[i]);
+                }
+                printf("\n");
+
+                //-------------------------------------------------------------------------
+                printf("Calling pmSubtractionRejectStamps() with acceptable input parameters.  Should return TRUE.\n");
+                rc = pmSubtractionRejectStamps(stamps, maskImg, 0xff, TST03_FOOTPRINT, 1.0, refImg,
+                                               tstImg, solution, myKernels);
+                if (rc != TRUE) {
+                    printf("TEST ERROR: pmSubtractionRejectStamps() returned FALSE.\n");
+                    testStatus = true;
+                } else {}
+
+                //-------------------------------------------------------------------------
+                printf("Calling pmSubtractionKernelImage() with NULL solution.  Should generate error, return NULL.\n");
+                psImage *kernelImg = pmSubtractionKernelImage(NULL, NULL, myKernels, 0.0, 0.0);
+                if (kernelImg != NULL) {
+                    printf("TEST ERROR: pmSubtractionKernelImage() returned non-NULL.\n");
+                    testStatus = true;
+                }
+                free(kernelImg);
+
+                //-------------------------------------------------------------------------
+                printf("Calling pmSubtractionKernelImage() with NULL kernels.  Should generate error, return NULL.\n");
+                kernelImg = pmSubtractionKernelImage(NULL, solution, NULL, 0.0, 0.0);
+                if (kernelImg != NULL) {
+                    printf("TEST ERROR: pmSubtractionKernelImage() returned non-NULL.\n");
+                    testStatus = true;
+                }
+                free(kernelImg);
+
+                //-------------------------------------------------------------------------
+                printf("Calling pmSubtractionKernelImage() unallowable x value.  Should generate error, return NULL.\n");
+                kernelImg = pmSubtractionKernelImage(NULL, solution, myKernels, -2.0, 0.0);
+                if (kernelImg != NULL) {
+                    printf("TEST ERROR: pmSubtractionKernelImage() returned non-NULL.\n");
+                    testStatus = true;
+                }
+                free(kernelImg);
+
+                //-------------------------------------------------------------------------
+                printf("Calling pmSubtractionKernelImage() unallowable x value.  Should generate error, return NULL.\n");
+                kernelImg = pmSubtractionKernelImage(NULL, solution, myKernels, 2.0, 0.0);
+                if (kernelImg != NULL) {
+                    printf("TEST ERROR: pmSubtractionKernelImage() returned non-NULL.\n");
+                    testStatus = true;
+                }
+                free(kernelImg);
+
+                //-------------------------------------------------------------------------
+                printf("Calling pmSubtractionKernelImage() unallowable y value.  Should generate error, return NULL.\n");
+                kernelImg = pmSubtractionKernelImage(NULL, solution, myKernels, 0.0, -2.0);
+                if (kernelImg != NULL) {
+                    printf("TEST ERROR: pmSubtractionKernelImage() returned non-NULL.\n");
+                    testStatus = true;
+                }
+                free(kernelImg);
+
+                //-------------------------------------------------------------------------
+                printf("Calling pmSubtractionKernelImage() unallowable y value.  Should generate error, return NULL.\n");
+                kernelImg = pmSubtractionKernelImage(NULL, solution, myKernels, 0.0, 2.0);
+                if (kernelImg != NULL) {
+                    printf("TEST ERROR: pmSubtractionKernelImage() returned non-NULL.\n");
+                    testStatus = true;
+                }
+                free(kernelImg);
+
+                //-------------------------------------------------------------------------
+                printf("Calling pmSubtractionKernelImage() with acceptable input parameters.  Should return a psImage.\n");
+                kernelImg = pmSubtractionKernelImage(NULL, solution, myKernels, 0.5, 0.5);
+                if (kernelImg == NULL) {
+                    printf("TEST ERROR: pmSubtractionKernelImage() returned NULL.\n");
+                    testStatus = true;
+                } else {
+                    for (psS32 row = 0 ; row < kernelImg->numRows; row++) {
+                        for (psS32 col = 0 ; col < kernelImg->numCols; col++) {
+                            printf("%f ", kernelImg->data.F32[row][col]);
+                        }
+                        printf("\n");
+                    }
+                }
+                free(kernelImg);
+
+                psFree(solution);
+            }
+        }
+    }
+    psFree(tstImg);
+    psFree(refImg);
+    psFree(stamps);
+    psFree(myKernels);
+    psFree(orders);
+    psFree(sigmas);
+
+    return(testStatus);
+}
+
+/*******************************************************************************
+NOTE: This function returns TRUE if there were no errors.
+ ******************************************************************************/
+int test03( void )
+{
+    bool testStatus = false;
+
+
+    srand(1995);
+    if (1)
+        testStatus|= testSubCalcEqu(TST03_NUM_COLS,
+                                    TST03_NUM_ROWS,
+                                    TST03_NUM_STAMPS_COLS,
+                                    TST03_NUM_STAMPS_ROWS,
+                                    TST03_BORDER,
+                                    PM_SUBTRACTION_KERNEL_POIS);
+
+
+    srand(1995);
+    if (1)
+        testStatus|= testSubCalcEqu(TST03_NUM_COLS,
+                                    TST03_NUM_ROWS,
+                                    TST03_NUM_STAMPS_COLS,
+                                    TST03_NUM_STAMPS_ROWS,
+                                    TST03_BORDER,
+                                    PM_SUBTRACTION_KERNEL_ISIS);
+
+
+
+    return(!testStatus);
+}
Index: /branches/eam_branch_20071023/psModules/test/imcombine/tst_pmReadoutCombine.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/imcombine/tst_pmReadoutCombine.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/imcombine/tst_pmReadoutCombine.c	(revision 15358)
@@ -0,0 +1,462 @@
+/** @file tst_pmReadoutCombine.c
+ *
+ *  test00() This routine will test the basic functionality.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-03-04 01:01:34 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ *  XXX: Untested:
+ * S16, S32 types
+ * Multiple input readouts with varying sizes and offsets.
+ * params->fracLow and params->fracHigh
+ * params->nKeep
+ * (gain > 0.0) && (readnoise >= 0.0) (applyZeroScale == true)
+ * (gain > 0.0) && (readnoise >= 0.0) (applyZeroScale == false)
+ *
+ */
+
+#include "psTest.h"
+#include "pslib.h"
+#include "pmReadoutCombine.h"
+static int test00(void);
+static int test01(void);
+testDescription tests[] = {
+                              {test00, 000, "pmSubtractBias(): Basic readout combines with no image overlap", true, false},
+                              {test01, 000, "pmSubtractBias(): input parameter error conditions", true, false},
+                              {NULL}
+                          };
+
+#define NUM_READOUTS  10
+#define INPUT_NUM_ROWS 20
+#define INPUT_NUM_COLS 20
+#define VEC_ZERO 1.0
+#define VEC_SCALE 2.0
+
+psS32 VerifyTheOutput(psImage *output, psF32 expect)
+{
+    bool testStatus = true;
+
+    for (psS32 i = 0 ; i < output->numRows ; i++) {
+        for (psS32 j = 0 ; j < output->numCols ; j++) {
+            if (output->data.F32[i][j] != expect) {
+                printf("TEST ERROR: output[%d][%d] is %.2f, should be %f\n", i, j, output->data.F32[i][j], expect);
+                testStatus = false;
+            }
+        }
+    }
+    return(testStatus);
+}
+
+
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    return !runTestSuite(stderr, "Test Point Driver", tests, argc, argv);
+}
+
+/******************************************************************************
+simpleCombineNoOverlap(): this routine creates a list of NUM_READOUTS input
+readouts and calls pmReadoutCombine().
+ *****************************************************************************/
+int simpleCombineNoOverlap(psS32 numInputCols, psS32 numInputRows)
+{
+    int i;
+    int r;
+    psList *list = NULL;
+    int baseRowsReadout[NUM_READOUTS];
+    int baseColsReadout[NUM_READOUTS];
+    int baseRows[NUM_READOUTS];
+    int baseCols[NUM_READOUTS];
+    int numRows[NUM_READOUTS];
+    int numCols[NUM_READOUTS];
+    int minOutRow = 10000;
+    int minOutCol = 10000;
+    int maxOutRow = -1;
+    int maxOutCol = -1;
+    psImage *output = NULL;
+    psCombineParams *params = (psCombineParams *) psAlloc(sizeof(psCombineParams));
+    psVector *zero = psVectorAlloc(NUM_READOUTS, PS_TYPE_F32);
+    psVector *scale = psVectorAlloc(NUM_READOUTS, PS_TYPE_F32);
+    zero->n = zero->nalloc;
+    scale->n = scale->nalloc;
+    printPositiveTestHeader(stdout, "pmReadoutCombine", "simpleCombineNoOverlap");
+
+    for (i=0;i<NUM_READOUTS;i++) {
+        zero->data.F32[i] = VEC_ZERO;
+    }
+    for (i=0;i<NUM_READOUTS;i++) {
+        scale->data.F32[i] = VEC_SCALE;
+    }
+
+    params->stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+    params->maskVal = 1;
+    params->fracLow = 0.0;
+    params->fracHigh = 10000.0;
+    params->nKeep = 0;
+
+    //
+    // Create a psList of psReadouts.  The pixels in readout r will all have the
+    // value r.
+    //
+    for (r=0;r<NUM_READOUTS;r++) {
+        baseRowsReadout[r] = r + 40;
+        baseColsReadout[r] = r + 42;
+        baseRows[r] = r;
+        baseCols[r] = r+2;
+        numRows[r] = 4 + (2 * r);
+        numCols[r] = 8 + (2 * r);
+
+        baseRowsReadout[r] = 0;
+        baseColsReadout[r] = 0;
+        baseRows[r] = 0;
+        baseCols[r] = 0;
+        numRows[r] = numInputRows;
+        numCols[r] = numInputCols;
+
+        psImage *tmpImage = psImageAlloc(numCols[r], numRows[r], PS_TYPE_F32);
+        PS_IMAGE_SET_F32(tmpImage, ((float) r));
+        *(int *) (& (tmpImage->row0)) = baseRows[r];
+        *(int *) (& (tmpImage->col0)) = baseCols[r];
+        pmReadout *tmpReadout = pmReadoutAlloc(NULL);
+        tmpReadout->row0 = 0;
+        tmpReadout->col0 = 0;
+        tmpReadout->image = tmpImage;
+
+        minOutRow = PS_MIN(minOutRow, (baseRowsReadout[r] + baseRows[r]));
+        minOutCol = PS_MIN(minOutCol, (baseColsReadout[r] + baseCols[r]));
+        maxOutRow = PS_MAX(maxOutRow, (baseRowsReadout[r] + baseRows[r] + numRows[r]));
+        maxOutCol = PS_MAX(maxOutCol, (baseColsReadout[r] + baseCols[r] + numCols[r]));
+
+        if (r == 0) {
+            list = psListAlloc(tmpReadout);
+        } else {
+            psListAdd(list, PS_LIST_HEAD, tmpReadout);
+        }
+    }
+    printf("tst_pmReadoutCombine(): (minOutRow, minOutCol) to (maxOutRow, maxOutCol) is (%d, %d) (%d, %d)\n",
+           minOutRow, minOutCol, maxOutRow, maxOutCol);
+
+    output = pmReadoutCombine(output, list, params, zero, scale, true, 0.0, 0.0);
+    psF32 NR = (psF32) NUM_READOUTS;
+    psF32 expectedPixel = ((NR/2.0) * (VEC_ZERO + (VEC_ZERO + VEC_SCALE * (NR - 1)))) / NR;
+
+    int testStatus = VerifyTheOutput(output, expectedPixel);
+
+    psFree(params->stats);
+    psFree(params);
+    psFree(output);
+    psFree(zero);
+    psFree(scale);
+
+    psListElem *tmpInput = (psListElem *) list->head;
+    while (NULL != tmpInput) {
+        pmReadout *tmpReadout = (pmReadout *) tmpInput->data;
+        psFree(tmpReadout);
+        tmpInput = tmpInput->next;
+    }
+    psFree(list);
+
+    printFooter(stdout, "pmReadoutCombine", "simpleCombineNoOverlap", true);
+    return(testStatus);
+}
+
+int test00( void )
+{
+    int testStatus = 0;
+
+    testStatus |= simpleCombineNoOverlap(1, 1);
+    testStatus |= simpleCombineNoOverlap(INPUT_NUM_COLS, 1);
+    testStatus |= simpleCombineNoOverlap(1, INPUT_NUM_ROWS);
+    testStatus |= simpleCombineNoOverlap(INPUT_NUM_COLS, INPUT_NUM_ROWS);
+
+    return(testStatus);
+}
+
+/******************************************************************************
+test01(): we simply call pmReadoutCombine() with a variety of erroneous input
+parameter combinations and verify that it behaves properly.
+ *****************************************************************************/
+int test01()
+{
+    int testStatus = true;
+    int i;
+    int r;
+    psList *list = NULL;
+    int baseRowsReadout[NUM_READOUTS];
+    int baseColsReadout[NUM_READOUTS];
+    int baseRows[NUM_READOUTS];
+    int baseCols[NUM_READOUTS];
+    int numRows[NUM_READOUTS];
+    int numCols[NUM_READOUTS];
+    int minOutRow = 10000;
+    int minOutCol = 10000;
+    int maxOutRow = -1;
+    int maxOutCol = -1;
+    psImage *output = NULL;
+    psImage *rc = NULL;
+    psCombineParams *params = (psCombineParams *) psAlloc(sizeof(psCombineParams));
+    psVector *zero = psVectorAlloc(NUM_READOUTS, PS_TYPE_F32);
+    psVector *zeroHalf = psVectorAlloc(NUM_READOUTS/2, PS_TYPE_F32);
+    psVector *zeroBig = psVectorAlloc(NUM_READOUTS+1, PS_TYPE_F32);
+    psVector *zeroF64 = psVectorAlloc(NUM_READOUTS, PS_TYPE_F64);
+    psVector *scale = psVectorAlloc(NUM_READOUTS, PS_TYPE_F32);
+    psVector *scaleHalf = psVectorAlloc(NUM_READOUTS/2, PS_TYPE_F32);
+    psVector *scaleBig = psVectorAlloc(NUM_READOUTS*2, PS_TYPE_F32);
+    psVector *scaleF64 = psVectorAlloc(NUM_READOUTS, PS_TYPE_F64);
+    zero->n = zero->nalloc;
+    zeroHalf->n = zeroHalf->nalloc;
+    zeroBig->n = zeroBig->nalloc;
+    zeroF64->n = zeroF64->nalloc;
+    scale->n = scale->nalloc;
+    scaleHalf->n = scaleHalf->nalloc;
+    scaleBig->n = scaleBig->nalloc;
+    scaleF64->n = scaleF64->nalloc;
+    for (i=0;i<NUM_READOUTS;i++) {
+        zero->data.F32[i] = 3.0;
+        zeroBig->data.F32[i] = 3.0;
+        zero->data.F32[i] = VEC_ZERO;
+        zeroBig->data.F32[i] = VEC_ZERO;
+    }
+    for (i=0;i<NUM_READOUTS;i++) {
+        scale->data.F32[i] = 6.0;
+        scaleBig->data.F32[i] = 6.0;
+        scale->data.F32[i] = VEC_SCALE;
+        scaleBig->data.F32[i] = VEC_SCALE;
+    }
+    printPositiveTestHeader(stdout, "pmReadoutCombine", "Testing bad input parameter conditions");
+
+    params->stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+    params->maskVal = 1;
+    params->fracLow = 0.0;
+    params->fracHigh = 10000.0;
+    params->nKeep = 0;
+
+    for (r=0;r<NUM_READOUTS;r++) {
+        baseRowsReadout[r] = 0;
+        baseColsReadout[r] = 0;
+        baseRows[r] = 0;
+        baseCols[r] = 0;
+        numRows[r] = INPUT_NUM_ROWS;
+        numCols[r] = INPUT_NUM_COLS;
+
+        psImage *tmpImage = psImageAlloc(numCols[r], numRows[r], PS_TYPE_F32);
+        PS_IMAGE_SET_F32(tmpImage, ((float) r));
+        *(int *) (& (tmpImage->row0)) = baseRows[r];
+        *(int *) (& (tmpImage->col0)) = baseCols[r];
+        pmReadout *tmpReadout = pmReadoutAlloc(NULL);
+        tmpReadout->row0 = 0;
+        tmpReadout->col0 = 0;
+        tmpReadout->image = tmpImage;
+        minOutRow = PS_MIN(minOutRow, (baseRowsReadout[r] + baseRows[r]));
+        minOutCol = PS_MIN(minOutCol, (baseColsReadout[r] + baseCols[r]));
+        maxOutRow = PS_MAX(maxOutRow, (baseRowsReadout[r] + baseRows[r] + numRows[r]));
+        maxOutCol = PS_MAX(maxOutCol, (baseColsReadout[r] + baseCols[r] + numCols[r]));
+
+        if (r == 0) {
+            list = psListAlloc(tmpReadout);
+        } else {
+            psListAdd(list, PS_LIST_HEAD, tmpReadout);
+        }
+    }
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() with NULL zero vector.\n");
+    rc = pmReadoutCombine(NULL, list, params, NULL, scale, true, 0.0, 0.0);
+    if (rc != NULL) {
+        psF32 NR = (psF32) NUM_READOUTS;
+        psF32 expectedPixel = ((NR/2.0) * (0.0 + (0.0 + VEC_SCALE * (NR - 1)))) / NR;
+        if (false == VerifyTheOutput(rc, expectedPixel)) {
+            testStatus = false;
+        }
+
+        if (rc->type.type != scale->type.type) {
+            printf("TEST ERROR: output readout->image has incorrect type.\n");
+            testStatus = false;
+        }
+        psFree(rc);
+    } else {
+        printf("TEST ERROR: pmReadoutCombine() returned NULL\n");
+        testStatus = false;
+
+    }
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() with incorrect length zero vector (too small).  Should generate error.\n");
+    rc = pmReadoutCombine(NULL, list, params, zeroHalf, scale, true, 0.0, 0.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmReadoutCombine() did not return NULL\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() with incorrect type zero vector.  Should generate error.\n");
+    rc = pmReadoutCombine(NULL, list, params, zeroF64, scale, true, 0.0, 0.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmReadoutCombine() did not return NULL\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() with incorrect length zero vector (too big).  Should generate warning.\n");
+    rc = pmReadoutCombine(output, list, params, zeroBig, scale, true, 0.0, 0.0);
+    if (rc != NULL) {
+        psF32 NR = (psF32) NUM_READOUTS;
+        psF32 expectedPixel = ((NR/2.0) * (VEC_ZERO + (VEC_ZERO + VEC_SCALE * (NR - 1)))) / NR;
+
+        if (false == VerifyTheOutput(rc, expectedPixel)) {
+            testStatus = false;
+        }
+        psFree(rc);
+        rc = NULL;
+    } else {
+        printf("TEST ERROR: pmReadoutCombine() returned NULL\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() with NULL scale vector.\n");
+    rc = pmReadoutCombine(output, list, params, zero, NULL, true, 0.0, 0.0);
+
+    if (rc != NULL) {
+        psF32 NR = (psF32) NUM_READOUTS;
+        psF32 expectedPixel = ((NR/2.0) * (VEC_ZERO + (VEC_ZERO + 1.0 * (NR - 1)))) / NR;
+        if (false == VerifyTheOutput(rc, expectedPixel)) {
+            testStatus = false;
+        }
+
+        if (rc->type.type != scale->type.type) {
+            printf("TEST ERROR: output readout->image has incorrect type.\n");
+            testStatus = false;
+        }
+        psFree(rc);
+    } else {
+        printf("TEST ERROR: pmReadoutCombine() returned NULL\n");
+        testStatus = false;
+
+    }
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() with incorrect length scale vector (too small).  Should generate error.\n");
+    rc = pmReadoutCombine(output, list, params, zero, scaleHalf, true, 0.0, 0.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmReadoutCombine() did not return NULL\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() with incorrect type scale vector.  Should generate error.\n");
+    rc = pmReadoutCombine(output, list, params, zero, scaleF64, true, 0.0, 0.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmReadoutCombine() did not return NULL\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() with incorrect length scale vector (too big).  Should generate warning.\n");
+    rc = pmReadoutCombine(output, list, params, zero, scaleBig, true, 0.0, 0.0);
+    if (rc != NULL) {
+        psF32 NR = (psF32) NUM_READOUTS;
+        psF32 expectedPixel = ((NR/2.0) * (VEC_ZERO + (VEC_ZERO + VEC_SCALE * (NR - 1)))) / NR;
+
+        if (false == VerifyTheOutput(rc, expectedPixel)) {
+            testStatus = false;
+        }
+        psFree(rc);
+        rc = NULL;
+    } else {
+        printf("TEST ERROR: pmReadoutCombine() returned NULL\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() insufficient size output image.  Should generate error, return NULL.\n");
+    output = psImageAlloc(1, 1, PS_TYPE_F32);
+    rc = pmReadoutCombine(output, list, params, zero, scale, true, 0.0, 0.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmReadoutCombine() did not return NULL\n");
+        testStatus = false;
+    }
+    psFree(output);
+    output = NULL;
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() row0/col0 too large.  Should generate error, return NULL.\n");
+    output = psImageAlloc(1, 1, PS_TYPE_F32);
+    *(psS32*)&output->row0 = 10000;
+    *(psS32*)&output->col0 = 10000;
+    rc = pmReadoutCombine(output, list, params, zero, scale, true, 0.0, 0.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmReadoutCombine() did not return NULL\n");
+        testStatus = false;
+    }
+    psFree(output);
+    output = NULL;
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() with NULL input list.  Should generate error, return NULL.\n");
+    rc = pmReadoutCombine(output, NULL, params, zero, scale, true, 0.0, 0.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmReadoutCombine() did not return NULL\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("Calling pmReadoutCombine() with NULL params.  Should generate error, return NULL.\n");
+    rc = pmReadoutCombine(output, list, NULL, zero, scale, true, 0.0, 0.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmReadoutCombine() did not return NULL\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------\n");
+    psStatsOptions oldStatsOpts = params->stats->options |= PS_STAT_MIN;
+    printf("Calling pmReadoutCombine() with multiple stats->options.  Should generate error, return NULL.\n");
+    rc = pmReadoutCombine(output, list, params, zero, scale, true, 0.0, 0.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmReadoutCombine() did not return NULL\n");
+        testStatus = false;
+    }
+    params->stats->options = oldStatsOpts;
+
+    printf("----------------------------------------------------------------------------\n");
+    psStats *oldStats = params->stats;
+    printf("Calling pmReadoutCombine() with NULL param->stats.  Should generate error, return NULL.\n");
+    params->stats = NULL;
+    rc = pmReadoutCombine(output, list, params, zero, scale, true, 0.0, 0.0);
+    if (rc != NULL) {
+        printf("TEST ERROR: pmReadoutCombine() did not return NULL\n");
+        testStatus = false;
+    }
+    params->stats = oldStats;
+
+    printf("----------------------------------------------------------------------------\n");
+    printf("============================================================================\n");
+
+    psFree(params->stats);
+    psFree(params);
+    //    psFree(output);
+    //    psFree(rc);
+    psFree(zero);
+    psFree(zeroHalf);
+    psFree(zeroBig);
+    psFree(zeroF64);
+    psFree(scale);
+    psFree(scaleHalf);
+    psFree(scaleBig);
+    psFree(scaleF64);
+    psListElem *tmpInput = (psListElem *) list->head;
+    while (NULL != tmpInput) {
+        pmReadout *tmpReadout = (pmReadout *) tmpInput->data;
+        psFree(tmpReadout);
+        tmpInput = tmpInput->next;
+    }
+    psFree(list);
+
+    printFooter(stdout, "pmReadoutCombine", "Testing bad input parameter conditions", true);
+    return(testStatus);
+}
Index: /branches/eam_branch_20071023/psModules/test/objects/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/objects/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/objects/.cvsignore	(revision 15358)
@@ -0,0 +1,9 @@
+.deps
+.libs
+Makefile
+Makefile.in
+temp
+tap_pmGrowthCurve
+tap_pmSourceFitModel
+tap_pmSourceFitModel_Delta
+tap_pmSourcePhotometry
Index: /branches/eam_branch_20071023/psModules/test/objects/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/objects/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/objects/Makefile.am	(revision 15358)
@@ -0,0 +1,31 @@
+AM_CPPFLAGS = \
+	$(SRCINC) \
+	-I$(top_srcdir)/test/tap/src \
+	-I$(top_srcdir)/test/pstap/src \
+	$(PSMODULES_CFLAGS)
+
+AM_LDFLAGS = \
+	$(top_builddir)/src/libpsmodules.la  \
+	$(top_builddir)/test/tap/src/libtap.la \
+	$(top_builddir)/test/pstap/src/libpstap.la \
+	$(PSMODULES_LIBS)
+
+TEST_PROGS =
+
+if BUILD_TESTS
+bin_PROGRAMS = $(TEST_PROGS)
+TESTS = $(TEST_PROGS)
+else
+check_PROGRAMS = $(TEST_PROGS)
+endif
+
+CLEANFILES = $(check_DATA) temp/* core core.* *~ *.bb *.bbg *.da gmon.out
+
+test: check
+	$(top_srcdir)/test/test.pl
+
+# Removed
+#	tap_pmSourcePhotometry
+#	tap_pmGrowthCurve
+#	tap_pmSourceFitModel
+#	tap_pmSourceFitModel_Delta
Index: /branches/eam_branch_20071023/psModules/test/objects/tap_pmFringe.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/objects/tap_pmFringe.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/objects/tap_pmFringe.c	(revision 15358)
@@ -0,0 +1,131 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+#define NUM_ROWS 8
+#define NUM_COLS 16
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    plan_tests(35);
+
+
+    // Test pmFringeRegionsAlloc()
+    {
+        psMemId id = psMemGetId();
+        pmFringeRegions *fringe = pmFringeRegionsAlloc(1, 2, 3, 4, 5);
+        ok(fringe != NULL, "pmFringeRegionsAlloc() returned non-NULL");
+        ok(fringe->x == NULL, "pmFringeRegionsAlloc() set fringe->x correctly");
+        ok(fringe->y == NULL, "pmFringeRegionsAlloc() set fringe->y correctly");
+        ok(fringe->mask == NULL, "pmFringeRegionsAlloc() set fringe->mask correctly");
+        ok(fringe->nRequested == 1, "pmFringeRegionsAlloc() set fringe->nRequested correctly");
+        ok(fringe->nAccepted == 0, "pmFringeRegionsAlloc() set fringe->nAccepted correctly");
+        ok(fringe->dX == 2, "pmFringeRegionsAlloc() set fringe->dX correctly");
+        ok(fringe->dY == 3, "pmFringeRegionsAlloc() set fringe->dY correctly");
+        ok(fringe->nX == 4, "pmFringeRegionsAlloc() set fringe->nX correctly");
+        ok(fringe->nY == 5, "pmFringeRegionsAlloc() set fringe->nY correctly");
+        psFree(fringe);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    #define NUM_FRINGE_PNTS 10
+    #define DX 2
+    #define DY 2
+    #define NX 1
+    #define NY 1
+    // test pmFringeRegionsCreatePoints(): NULL random number generator,
+    // NULL fringe X, y, and mask vectors.
+    {
+        psMemId id = psMemGetId();
+        pmFringeRegions *fringe = pmFringeRegionsAlloc(NUM_FRINGE_PNTS, DX, DY, NX, NY);
+        ok(fringe != NULL, "pmFringeRegionsAlloc() returned non-NULL");
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        bool rc = pmFringeRegionsCreatePoints(fringe, img, NULL);
+        ok(rc, "pmFringeRegionsCreatePoints() returned TRUE");
+        ok(fringe->x != NULL &&
+           fringe->x->type.type == PS_TYPE_F32 &&
+           fringe->x->n ==NUM_FRINGE_PNTS, "pmFringeRegionsCreatePoints() returned correct fringe->x psVector");
+        ok(fringe->y != NULL &&
+           fringe->y->type.type == PS_TYPE_F32 &&
+           fringe->y->n ==NUM_FRINGE_PNTS, "pmFringeRegionsCreatePoints() returned correct fringe->y psVector");
+        ok(fringe->mask != NULL &&
+           fringe->mask->type.type == PS_TYPE_MASK &&
+           fringe->mask->n ==NUM_FRINGE_PNTS, "pmFringeRegionsCreatePoints() returned correct fringe->mask psVector");
+        bool errorFlag = false;
+        for (int i = 0 ; i < NUM_FRINGE_PNTS ; i++) {
+            if (fringe->mask->data.U8[i] != 0) {
+                diag("ERROR: pmFringeRegionsCreatePoints() did not set mask[%d] to 0", i);
+                errorFlag = true;
+            }
+            if (!((fringe->x->data.F32[i] >= DX) &&
+                 (fringe->x->data.F32[i] <= NUM_COLS-DX))) {
+                diag("ERROR: pmFringeRegionsCreatePoints() did not set x[%d] correctly.  It was %.2f, should be within (%d %d)", i,
+                      fringe->x->data.F32[i], DX, NUM_COLS-DX);
+                errorFlag = true;
+            }
+            if (!((fringe->y->data.F32[i] >= DY) &&
+                 (fringe->y->data.F32[i] <= NUM_ROWS-DY))) {
+                diag("ERROR: pmFringeRegionsCreatePoints() did not set x[%d] correctly.  It was %.2f, should be within (%d %d)", i,
+                      fringe->y->data.F32[i], DY, NUM_ROWS-DY);
+                errorFlag = true;
+            }
+        }
+
+        psFree(img);
+        psFree(fringe);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // test pmFringeRegionsCreatePoints(): non-NULL random number generator,
+    // non-NULL fringe X, y, and mask vectors.
+    {
+        psMemId id = psMemGetId();
+        pmFringeRegions *fringe = pmFringeRegionsAlloc(NUM_FRINGE_PNTS, DX, DY, NX, NY);
+        ok(fringe != NULL, "pmFringeRegionsAlloc() returned non-NULL");
+        psImage *img = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 10);
+        fringe->x = psVectorAlloc(NUM_FRINGE_PNTS/2, PS_TYPE_F32);
+        fringe->y = psVectorAlloc(NUM_FRINGE_PNTS/2, PS_TYPE_F32);
+        fringe->mask = psVectorAlloc(NUM_FRINGE_PNTS/2, PS_TYPE_MASK);
+        bool rc = pmFringeRegionsCreatePoints(fringe, img, NULL);
+        ok(rc, "pmFringeRegionsCreatePoints() returned TRUE");
+        ok(fringe->x != NULL &&
+           fringe->x->type.type == PS_TYPE_F32 &&
+           fringe->x->n ==NUM_FRINGE_PNTS, "pmFringeRegionsCreatePoints() returned correct fringe->x psVector");
+        ok(fringe->y != NULL &&
+           fringe->y->type.type == PS_TYPE_F32 &&
+           fringe->y->n ==NUM_FRINGE_PNTS, "pmFringeRegionsCreatePoints() returned correct fringe->y psVector");
+        ok(fringe->mask != NULL &&
+           fringe->mask->type.type == PS_TYPE_MASK &&
+           fringe->mask->n ==NUM_FRINGE_PNTS, "pmFringeRegionsCreatePoints() returned correct fringe->mask psVector");
+        bool errorFlag = false;
+        for (int i = 0 ; i < NUM_FRINGE_PNTS ; i++) {
+            if (fringe->mask->data.U8[i] != 0) {
+                diag("ERROR: pmFringeRegionsCreatePoints() did not set mask[%d] to 0", i);
+                errorFlag = true;
+            }
+            if (!((fringe->x->data.F32[i] >= DX) &&
+                 (fringe->x->data.F32[i] <= NUM_COLS-DX))) {
+                diag("ERROR: pmFringeRegionsCreatePoints() did not set x[%d] correctly.  It was %.2f, should be within (%d %d)", i,
+                      fringe->x->data.F32[i], DX, NUM_COLS-DX);
+                errorFlag = true;
+            }
+            if (!((fringe->y->data.F32[i] >= DY) &&
+                 (fringe->y->data.F32[i] <= NUM_ROWS-DY))) {
+                diag("ERROR: pmFringeRegionsCreatePoints() did not set x[%d] correctly.  It was %.2f, should be within (%d %d)", i,
+                      fringe->y->data.F32[i], DY, NUM_ROWS-DY);
+                errorFlag = true;
+            }
+        }
+        psFree(rng);
+        psFree(img);
+        psFree(fringe);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+//HERE
Index: /branches/eam_branch_20071023/psModules/test/objects/tap_pmGrowthCurve.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/objects/tap_pmGrowthCurve.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/objects/tap_pmGrowthCurve.c	(revision 15358)
@@ -0,0 +1,321 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+int main (void)
+{
+    pmModelGroupInit ();
+
+    plan_tests(57);
+
+    diag("pmGrowthCurve tests");
+
+    // test allocation
+    diag("offset of 0.0,0.0 wrt growth ref source");
+    {
+        psMemId id = psMemGetId();
+
+        pmGrowthCurve *growth = pmGrowthCurveAlloc (2.0, 100.0, 15.0);
+
+        ok(growth != NULL, "growth curve allocated");
+        skip_start(growth == NULL, 0, "Skipping tests because pmShutterCorrParsAlloc() failed");
+
+        ok(growth->radius->n == (100.0 - 2.0), "correct number of growth radii");
+        ok(growth->apMag->n == (100.0 - 2.0), "correct number of growth apMags");
+
+        ok_float(growth->refRadius, 15.0, "correct refRadius");
+        ok_float(growth->maxRadius, 100.0, "correct maxRadius");
+
+        // does the growth curve correctly fix aperture mags?
+
+        // generate a simple readout
+        pmReadout *readout = pmReadoutAlloc (NULL);
+        readout->image = psImageAlloc (64, 64, PS_TYPE_F32);
+        readout->mask  = psImageAlloc (64, 64, PS_TYPE_U8);
+
+        // create an empty reference image
+        psImageInit (readout->image, 0.0);
+        psImageInit (readout->mask, 0);
+
+        // generate a simple psf
+        pmPSF *psf = pmPSFBuildSimple ("PS_MODEL_GAUSS", 1.5, 1.5, 0.0);
+        psf->growth = growth;
+
+        pmGrowthCurveGenerate (readout, psf, false);
+
+        // check ap mags for a few radii set by hand
+        ok_float_tol(growth->apMag->data.F32[0],   -9.7805, 0.0001, "apMag at radius 0: %f", growth->apMag->data.F32[0]);
+        ok_float_tol(growth->apMag->data.F32[3],  -10.3722, 0.0001, "apMag at radius 3: %f", growth->apMag->data.F32[3]);
+        ok_float_tol(growth->apMag->data.F32[10], -10.3759, 0.0001, "apMag at radius 10: %f", growth->apMag->data.F32[10]);
+        ok_float_tol(growth->apMag->data.F32[30], -10.3759, 0.0001, "apMag at radius 30: %f", growth->apMag->data.F32[30]);
+
+        ok_float_tol(growth->apRef,  -10.3759, 0.0001, "apMag at ref radius : %f", growth->apRef);
+        ok_float_tol(growth->fitMag, -10.3759, 0.0001, "fitMag : %f", growth->fitMag);
+        ok_float(growth->apLoss, 0.0, "apLoss : %f", growth->apLoss);
+
+        // create template model and measure apMag at fractional offsets
+        // XXX note model is at 0.5,0.5 subpix center
+        pmModel *modelRef = pmModelAlloc(psf->type);
+        modelRef->params->data.F32[PM_PAR_SKY] = 0;
+        modelRef->params->data.F32[PM_PAR_I0] = 1000;
+        modelRef->params->data.F32[PM_PAR_XPOS] = 32.5;
+        modelRef->params->data.F32[PM_PAR_YPOS] = 32.5;
+
+        // measure growth-corrected photometry:
+        pmSource *source = pmSourceAlloc ();
+        source->modelPSF = pmModelFromPSF (modelRef, psf);
+        source->type = PM_SOURCE_TYPE_STAR;
+        source->pixels = psMemIncrRefCounter (readout->image);
+        source->mask = psMemIncrRefCounter (readout->mask);
+
+        source->modelPSF->dparams->data.F32[PM_PAR_I0] = 1;
+        source->mode = PM_SOURCE_MODE_SUBTRACTED;
+
+        source->modelPSF->radiusFit = 15.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        double refMag = source->apMag;
+
+        source->modelPSF->radiusFit = 10.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 8.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 6.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0003, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 4.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0020, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 3.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, -0.0001, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 2.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, -0.0075, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        // XXX include some apertures outside of growth correction range
+
+        psFree(modelRef);
+        psFree(source);
+        psFree(readout);
+        psFree(psf);
+
+        skip_end();
+
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // test allocation
+    diag("offset of 0.2,0.2 wrt growth ref source");
+    {
+        psMemId id = psMemGetId();
+
+        pmGrowthCurve *growth = pmGrowthCurveAlloc (2.0, 100.0, 15.0);
+
+        ok(growth != NULL, "growth curve allocated");
+        skip_start(growth == NULL, 0, "Skipping tests because pmShutterCorrParsAlloc() failed");
+
+        ok(growth->radius->n == (100.0 - 2.0), "correct number of growth radii");
+        ok(growth->apMag->n == (100.0 - 2.0), "correct number of growth apMags");
+
+        ok_float(growth->refRadius, 15.0, "correct refRadius");
+        ok_float(growth->maxRadius, 100.0, "correct maxRadius");
+
+        // does the growth curve correctly fix aperture mags?
+
+        // generate a simple readout
+        pmReadout *readout = pmReadoutAlloc (NULL);
+        readout->image = psImageAlloc (64, 64, PS_TYPE_F32);
+        readout->mask  = psImageAlloc (64, 64, PS_TYPE_U8);
+
+        // create an empty reference image
+        psImageInit (readout->image, 0.0);
+        psImageInit (readout->mask, 0);
+
+        // generate a simple psf
+        pmPSF *psf = pmPSFBuildSimple ("PS_MODEL_GAUSS", 1.5, 1.5, 0.0);
+        psf->growth = growth;
+
+        pmGrowthCurveGenerate (readout, psf, false);
+
+        // check ap mags for a few radii set by hand
+        ok_float_tol(growth->apMag->data.F32[0],   -9.7805, 0.0001, "apMag at radius 0: %f", growth->apMag->data.F32[0]);
+        ok_float_tol(growth->apMag->data.F32[3],  -10.3722, 0.0001, "apMag at radius 3: %f", growth->apMag->data.F32[3]);
+        ok_float_tol(growth->apMag->data.F32[10], -10.3759, 0.0001, "apMag at radius 10: %f", growth->apMag->data.F32[10]);
+        ok_float_tol(growth->apMag->data.F32[30], -10.3759, 0.0001, "apMag at radius 30: %f", growth->apMag->data.F32[30]);
+
+        ok_float_tol(growth->apRef,  -10.3759, 0.0001, "apMag at ref radius : %f", growth->apRef);
+        ok_float_tol(growth->fitMag, -10.3759, 0.0001, "fitMag : %f", growth->fitMag);
+        ok_float(growth->apLoss, 0.0, "apLoss : %f", growth->apLoss);
+
+        // create template model and measure apMag at fractional offsets
+        // XXX note model is at 0.5,0.5 subpix center
+        pmModel *modelRef = pmModelAlloc(psf->type);
+        modelRef->params->data.F32[PM_PAR_SKY] = 0;
+        modelRef->params->data.F32[PM_PAR_I0] = 1000;
+        modelRef->params->data.F32[PM_PAR_XPOS] = 32.3;
+        modelRef->params->data.F32[PM_PAR_YPOS] = 32.3;
+
+        // measure growth-corrected photometry:
+        pmSource *source = pmSourceAlloc ();
+        source->modelPSF = pmModelFromPSF (modelRef, psf);
+        source->type = PM_SOURCE_TYPE_STAR;
+        source->pixels = psMemIncrRefCounter (readout->image);
+        source->mask = psMemIncrRefCounter (readout->mask);
+
+        source->modelPSF->dparams->data.F32[PM_PAR_I0] = 1;
+        source->mode = PM_SOURCE_MODE_SUBTRACTED;
+
+        source->modelPSF->radiusFit = 15.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        double refMag = source->apMag;
+
+        source->modelPSF->radiusFit = 10.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 8.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 6.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0004, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 4.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0026, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 3.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, -0.0001, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 2.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, -0.0103, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        // XXX include some apertures outside of growth correction range
+
+        psFree(modelRef);
+        psFree(source);
+        psFree(readout);
+        psFree(psf);
+
+        skip_end();
+
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // test allocation
+    diag("offset of 0.4,0.4 wrt growth ref source");
+    {
+        psMemId id = psMemGetId();
+
+        pmGrowthCurve *growth = pmGrowthCurveAlloc (2.0, 100.0, 15.0);
+
+        ok(growth != NULL, "growth curve allocated");
+        skip_start(growth == NULL, 0, "Skipping tests because pmShutterCorrParsAlloc() failed");
+
+        ok(growth->radius->n == (100.0 - 2.0), "correct number of growth radii");
+        ok(growth->apMag->n == (100.0 - 2.0), "correct number of growth apMags");
+
+        ok_float(growth->refRadius, 15.0, "correct refRadius");
+        ok_float(growth->maxRadius, 100.0, "correct maxRadius");
+
+        // does the growth curve correctly fix aperture mags?
+
+        // generate a simple readout
+        pmReadout *readout = pmReadoutAlloc (NULL);
+        readout->image = psImageAlloc (64, 64, PS_TYPE_F32);
+        readout->mask  = psImageAlloc (64, 64, PS_TYPE_U8);
+
+        // create an empty reference image
+        psImageInit (readout->image, 0.0);
+        psImageInit (readout->mask, 0);
+
+        // generate a simple psf
+        pmPSF *psf = pmPSFBuildSimple ("PS_MODEL_GAUSS", 1.5, 1.5, 0.0);
+        psf->growth = growth;
+
+        pmGrowthCurveGenerate (readout, psf, false);
+
+        // check ap mags for a few radii set by hand
+        ok_float_tol(growth->apMag->data.F32[0],   -9.7805, 0.0001, "apMag at radius 0: %f", growth->apMag->data.F32[0]);
+        ok_float_tol(growth->apMag->data.F32[3],  -10.3722, 0.0001, "apMag at radius 3: %f", growth->apMag->data.F32[3]);
+        ok_float_tol(growth->apMag->data.F32[10], -10.3759, 0.0001, "apMag at radius 10: %f", growth->apMag->data.F32[10]);
+        ok_float_tol(growth->apMag->data.F32[30], -10.3759, 0.0001, "apMag at radius 30: %f", growth->apMag->data.F32[30]);
+
+        ok_float_tol(growth->apRef,  -10.3759, 0.0001, "apMag at ref radius : %f", growth->apRef);
+        ok_float_tol(growth->fitMag, -10.3759, 0.0001, "fitMag : %f", growth->fitMag);
+        ok_float(growth->apLoss, 0.0, "apLoss : %f", growth->apLoss);
+
+        // create template model and measure apMag at fractional offsets
+        // XXX note model is at 0.5,0.5 subpix center
+        pmModel *modelRef = pmModelAlloc(psf->type);
+        modelRef->params->data.F32[PM_PAR_SKY] = 0;
+        modelRef->params->data.F32[PM_PAR_I0] = 1000;
+        modelRef->params->data.F32[PM_PAR_XPOS] = 32.1;
+        modelRef->params->data.F32[PM_PAR_YPOS] = 32.1;
+
+        // measure growth-corrected photometry:
+        pmSource *source = pmSourceAlloc ();
+        source->modelPSF = pmModelFromPSF (modelRef, psf);
+        source->type = PM_SOURCE_TYPE_STAR;
+        source->pixels = psMemIncrRefCounter (readout->image);
+        source->mask = psMemIncrRefCounter (readout->mask);
+
+        source->modelPSF->dparams->data.F32[PM_PAR_I0] = 1;
+        source->mode = PM_SOURCE_MODE_SUBTRACTED;
+
+        source->modelPSF->radiusFit = 15.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        double refMag = source->apMag;
+
+        source->modelPSF->radiusFit = 10.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 8.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 6.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0006, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 4.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0038, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 3.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, +0.0000, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        source->modelPSF->radiusFit = 2.0;
+        pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+        ok_float_tol(refMag - source->apMag, -0.0164, 0.0001, "growth offset is is %f", refMag - source->apMag);
+
+        // XXX include some apertures outside of growth correction range
+
+        psFree(modelRef);
+        psFree(source);
+        psFree(readout);
+        psFree(psf);
+
+        skip_end();
+
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/objects/tap_pmMoments.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/objects/tap_pmMoments.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/objects/tap_pmMoments.c	(revision 15358)
@@ -0,0 +1,34 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    plan_tests(11);
+
+    // Test pmMomentsAlloc()
+    {
+        psMemId id = psMemGetId();
+        pmMoments *tmpMoments = pmMomentsAlloc();
+        ok(tmpMoments != NULL, "pmMomentsAlloc() returned a non-NULL pmMoments");
+        skip_start(tmpMoments == NULL, 9, "Skipping tests because pmMomentsAlloc() returned NULL");
+        ok(tmpMoments->x == 0.0, "pmMomentsAlloc set->x correctly");
+        ok(tmpMoments->y == 0.0, "pmMomentsAlloc set->y correctly");
+        ok(tmpMoments->Sx == 0.0, "pmMomentsAlloc set->Sx correctly");
+        ok(tmpMoments->Sy == 0.0, "pmMomentsAlloc set->Sy correctly");
+        ok(tmpMoments->Sxy == 0.0, "pmMomentsAlloc set->Sxy correctly");
+        ok(tmpMoments->Sum == 0.0, "pmMomentsAlloc set->Sum correctly");
+        ok(tmpMoments->Peak == 0.0, "pmMomentsAlloc set->Peak correctly");
+        ok(tmpMoments->Sky == 0.0, "pmMomentsAlloc set->Sky correctly");
+        ok(tmpMoments->nPixels == 0, "pmMomentsAlloc set->nPixels correctly");
+        psFree(tmpMoments);
+        skip_end();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+}
+
Index: /branches/eam_branch_20071023/psModules/test/objects/tap_pmPeaks.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/objects/tap_pmPeaks.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/objects/tap_pmPeaks.c	(revision 15358)
@@ -0,0 +1,702 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "tap.h"
+#include "pstap.h"
+/* The following are tested:
+    pmPeakAlloc()
+    pmPeaksInVector()
+    pmPeaksInImage()
+    pmPeaksSubset()
+    pmPeaksCompareAscend()
+    pmPeaksCompareDescend()
+Must test
+    pmCullPeaks()
+*/
+
+#define TST01_VECTOR_LENGTH 10
+#define NUM_ROWS 10
+#define NUM_COLS 10
+#define TST02_NUM_ROWS 5
+#define TST02_NUM_COLS 5
+
+/******************************************************************************
+test01(): we first test pmPeaksInVector() with a variety of bad input
+parameters.  Then we test it with a simple vector both 1- and multi-elements.
+ *****************************************************************************/
+bool test_pmPeaksInVector(int n)
+{
+    psMemId id = psMemGetId();
+    bool testStatus = true;
+    psVector *inData = psVectorAlloc(n, PS_TYPE_F32);
+    inData->n = inData->nalloc;
+    psVector *outData = NULL;
+
+    // Test first pixel peak.
+    for (psS32 i = 0 ; i < n ; i++) {
+        inData->data.F32[i] = (float) (n-i);
+    }
+    inData->data.F32[0] = (float) n;
+    outData= pmPeaksInVector(inData, 0.0);
+    if (outData == NULL) {
+        diag("TEST ERROR: pmPeaksInVector returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+        if (outData->n != 1) {
+            diag("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+        if (outData->data.U32[0] != 0) {
+            diag("TEST ERROR: Did not find peak at element 0.\n");
+            testStatus = false;
+        }
+        psFree(outData);
+    }
+
+
+    //
+    // Test first pixel peak, large threshold
+    //
+    for (psS32 i = 0 ; i < n ; i++) {
+        inData->data.F32[i] = (float) (n-i);
+    }
+    inData->data.F32[0] = (float) n;
+    outData= pmPeaksInVector(inData, (float) (n*n));
+    if (outData == NULL) {
+        diag("TEST ERROR: pmPeaksInVector returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+        if (outData->n != 0) {
+            diag("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+        psFree(outData);
+
+        // Skip remaining tests if the input vector has length 1.
+        if (n == 1) {
+            psFree(inData);
+            ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+            return(testStatus);
+        }
+    }
+
+    // Test last pixel peak.
+    for (psS32 i = 0 ; i < n ; i++) {
+        inData->data.F32[i] = (float) (i);
+    }
+    inData->data.F32[n-1] = (float) n;
+
+    outData= pmPeaksInVector(inData, 0.0);
+    if (outData == NULL) {
+        diag("TEST ERROR: pmPeaksInVector returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+        if (outData->n != 1) {
+            diag("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+        if (outData->data.U32[0] != n-1) {
+            diag("TEST ERROR: Did not find peak at element %d.\n", n-1);
+            testStatus = false;
+        }
+        psFree(outData);
+    }
+
+
+    // Test last pixel peak, large threshold.
+    for (psS32 i = 0 ; i < n ; i++) {
+        inData->data.F32[i] = (float) (i);
+    }
+    inData->data.F32[n-1] = (float) n;
+    outData= pmPeaksInVector(inData, (float) (n*n));
+    if (outData == NULL) {
+        diag("TEST ERROR: pmPeaksInVector returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+        if (outData->n != 0) {
+            diag("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+        psFree(outData);
+    }
+
+
+    // Test interior peaks.
+    // Set all even number elements to be peaks.
+    for (psS32 i = 0 ; i < n ; i++) {
+        if (0 == i%2) {
+            inData->data.F32[i] = (float) (2 * i);
+        } else {
+            inData->data.F32[i] = (float) (i);
+        }
+    }
+    inData->data.F32[0] = (float) n;
+
+
+    outData= pmPeaksInVector(inData, 0.0);
+    if (outData == NULL) {
+        diag("TEST ERROR: pmPeaksInVector returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+        if (outData->n != n/2) {
+            diag("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+        for (psS32 i = 0 ; i < outData->n ; i++) {
+            if (outData->data.U32[i] != (2 * i)) {
+                diag("TEST ERROR: the %d-th peak is element number %d\n", i, outData->data.U32[i]);
+                testStatus = false;
+            }
+        }
+        psFree(outData);
+    }
+
+
+    // Test interior peaks, with threshold = n*n.
+    // Should generate an empty output psVector.
+    outData= pmPeaksInVector(inData, (float) (n*n));
+    if (outData == NULL) {
+        diag("TEST ERROR: pmPeaksInVector returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+        if (outData->n != 0) {
+            diag("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+        psFree(outData);
+    }
+    psFree(inData);
+    ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    return(testStatus);
+}
+
+
+bool test_pmPeaksInImage(int numRows, int numCols)
+{
+    psMemId id = psMemGetId();
+    bool testStatus = true;
+    psImage *inData = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psArray *outData = NULL;
+    // Initialize test image.
+    for (psS32 i = 0 ; i < numRows ; i++) {
+        for (psS32 j = 0 ; j < numCols ; j++) {
+            inData->data.F32[i][j] = PS_SQR(i - numRows/2) + PS_SQR(j-numCols/2);
+        }
+    }
+
+    // Set corner and center pixels as peaks.
+    inData->data.F32[0][0] = PS_SQR(numRows) + PS_SQR(numCols);
+    inData->data.F32[0][numCols-1] = PS_SQR(numRows) + PS_SQR(numCols);
+    inData->data.F32[numRows-1][0] = PS_SQR(numRows) + PS_SQR(numCols);
+    inData->data.F32[numRows-1][numCols-1] = PS_SQR(numRows) + PS_SQR(numCols);
+    inData->data.F32[numRows/2][numCols/2] = PS_SQR(numRows) + PS_SQR(numCols);
+
+    // Call pmPeaksInImage() with a threshold of 0.0.
+    outData = pmPeaksInImage(inData, 0.0);
+
+    if (outData == NULL) {
+        diag("TEST ERROR: pmPeaksInImage returned a NULL psList.\n");
+        testStatus = false;
+    } else {
+        psS32 expectedNumPeaks;
+        if ((numRows == 1) && (numCols == 1)) {
+            expectedNumPeaks = 1;
+        } else if ((numRows == 1) || (numCols == 1)) {
+            expectedNumPeaks = 3;
+        } else {
+            expectedNumPeaks = 5;
+        }
+        if (outData->n != expectedNumPeaks) {
+            diag("TEST ERROR: pmPeaksInImage found %ld peaks (should be %d)\n", outData->n, expectedNumPeaks);
+            testStatus = false;
+        }
+
+        // HEY: verify
+        for (psS32 i = 0 ; i < outData->n ; i++) {
+            pmPeak *tmpPeak = (pmPeak *) outData->data[i];
+            if (((tmpPeak->x == 0) && (tmpPeak->y == 0)) ||
+                    ((tmpPeak->x == 0) && (tmpPeak->y == numRows-1)) ||
+                    ((tmpPeak->x == numCols-1) && (tmpPeak->y == 0)) ||
+                    ((tmpPeak->x == numCols-1) && (tmpPeak->y == numRows-1))) {
+                if (!((tmpPeak->type & PM_PEAK_LONE) || (tmpPeak->type & PM_PEAK_EDGE))) {
+                    diag("TEST ERROR: (0) peak at (%d, %d) (%f) ->type set improperly (0x%x).",
+                          tmpPeak->y, tmpPeak->x, tmpPeak->value, tmpPeak->type);
+                    diag(" should be (0x%x or 0x%x).\n", PM_PEAK_LONE, PM_PEAK_EDGE);
+                    testStatus = false;
+                }
+            } else if ((tmpPeak->x == numCols/2) && (tmpPeak->y == numRows/2)) {
+                if (tmpPeak->type != PM_PEAK_LONE) {
+                    diag("TEST ERROR: (1) peak at (%d, %d) (%f) ->type set improperly (0x%x).\n",
+                           tmpPeak->y, tmpPeak->x, tmpPeak->value, tmpPeak->type);
+                    diag(" should be (0x%x).\n", PM_PEAK_LONE);
+                    testStatus = false;
+                }
+            } else {
+                diag("TEST ERROR: Peak at (%d, %d) (%f)\n", tmpPeak->y, tmpPeak->x, tmpPeak->value);
+                testStatus = false;
+            }
+        }
+    }
+    psFree(inData);
+    psFree(outData);
+    ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    return(testStatus);
+}
+
+
+
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    psLogSetLevel(PS_LOG_INFO);
+    plan_tests(35);
+
+
+    // Test pmPeakAlloc()
+    {
+        psMemId id = psMemGetId();
+        pmPeak *tmpPeak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        ok(tmpPeak != NULL, "pmPeakAlloc() returned a non-NULL pmPeak");
+        skip_start(tmpPeak == NULL, 9, "Skipping tests because pmPeakAlloc() returned NULL");
+        ok(tmpPeak->id == 1, "pmPeakAlloc() set pmPeak->id");
+        ok(tmpPeak->x == 1, "pmPeakAlloc() set pmPeak->x");
+        ok(tmpPeak->y == 2, "pmPeakAlloc() set pmPeak->y");
+        ok(tmpPeak->value == 3.0, "pmPeakAlloc() set pmPeak->value");
+        ok(tmpPeak->flux == 0, "pmPeakAlloc() pmPeak->flux");
+        ok(tmpPeak->SN == 0, "pmPeakAlloc() pmPeak->SN");
+        ok(tmpPeak->xf == 1, "pmPeakAlloc() pmPeak->xf");
+        ok(tmpPeak->yf == 2, "pmPeakAlloc() pmPeak->yF");
+        ok(tmpPeak->type == PM_PEAK_LONE, "pmPeakAlloc() pmPeak->type");
+        psFree(tmpPeak);
+        skip_end();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Test pmPeakAlloc(): ensure pmPeak->id is properly incremented.
+    {
+        psMemId id = psMemGetId();
+        pmPeak *tmpPeak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+        skip_start(tmpPeak == NULL, 1, "Skipping tests because pmPeakAlloc() returned NULL");
+        ok(tmpPeak->id == 2, "pmPeakAlloc() incremented and set pmPeak->id");
+        psFree(tmpPeak);
+        skip_end();
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------
+    // pmPeaksInVector() tests
+    // Test pmPeaksInVector() with bad input parameters.
+    // Calling pmPeaksInVector with NULL psVector.  Should generate error.
+    {
+        psMemId id = psMemGetId();
+        psVector *tmpVec = pmPeaksInVector(NULL, 0.0);
+        ok(tmpVec == NULL, "pmPeaksInVector() returned a NULL with NULL psVector input");
+        psFree(tmpVec);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksInVector with empty psVector.  Should generate error.
+    {
+        psMemId id = psMemGetId();
+        psVector *tmpVecEmpty = psVectorAlloc(0, PS_TYPE_F32);
+        psVector *tmpVec = pmPeaksInVector(tmpVecEmpty, 0.0);
+        ok(tmpVec == NULL, "pmPeaksInVector() returned a NULL with NULL psVector input");
+        psFree(tmpVec);
+        psFree(tmpVecEmpty);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksInVector with PS_TYPE_F64 psVector.  Should generate error.
+    {
+        psMemId id = psMemGetId();
+        psVector *tmpVecF64 = psVectorAlloc(TST01_VECTOR_LENGTH, PS_TYPE_F64);
+        psVector *tmpVec = pmPeaksInVector(tmpVecF64, 0.0);
+        ok(tmpVec == NULL, "pmPeaksInVector() returned a NULL with F64 psVector input");
+        psFree(tmpVecF64);
+        psFree(tmpVec);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+    ok(test_pmPeaksInVector(1), "Tested pmPeaksInVector() on length 1 input vector");
+    ok(test_pmPeaksInVector(10), "Tested pmPeaksInVector() on length 10 input vector");
+
+
+
+    // ----------------------------------------
+    // pmPeaksInImage() tests
+    // Calling pmPeaksInImage with NULL psImage.  Should generate error.
+    {
+        psMemId id = psMemGetId();
+        psArray *tmpArray = pmPeaksInImage(NULL, 0.0);
+        ok(tmpArray == NULL, "pmPeaksInImage() returned NULL with NULL input image");
+        psFree(tmpArray);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksInImage with empty psImage.  Should generate error.
+    {
+        psMemId id = psMemGetId();
+        psImage *tmpImageEmpty = psImageAlloc(0, 0, PS_TYPE_F32);
+        psArray *tmpArray = pmPeaksInImage(tmpImageEmpty, 0.0);
+        ok(tmpArray == NULL, "pmPeaksInImage() returned NULL with empty input image");
+        psFree(tmpArray);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+    
+
+    // Calling pmPeaksInImage with PS_TYPE_F64 psImage.  Should generate error
+    {
+        psMemId id = psMemGetId();
+        psImage *tmpImageF64 = psImageAlloc(TST02_NUM_ROWS, TST02_NUM_COLS, PS_TYPE_F64);
+        psArray *tmpArray = pmPeaksInImage(tmpImageF64, 0.0);
+        ok(tmpArray == NULL, "pmPeaksInImage() returned NULL with F64 input image");
+        psFree(tmpImageF64);
+        psFree(tmpArray);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // XXX: Uncomment these and debug
+    //    testStatus&= test_pmPeaksInImage(1, 1);
+    //    testStatus&= test_pmPeaksInImage(2, 5);
+    //    testStatus&= test_pmPeaksInImage(5, 2);
+    // HEY: add code for small images
+    //    testStatus&= test_pmPeaksInImage(1, 1);
+    //    testStatus&= test_pmPeaksInImage(1, 8);
+    //    testStatus&= test_pmPeaksInImage(8, 1);
+    ok(test_pmPeaksInImage(TST02_NUM_ROWS, TST02_NUM_COLS),
+      "Tested pmPeaksInImage() on (%d, %d) image", TST02_NUM_ROWS, TST02_NUM_COLS);
+    ok(test_pmPeaksInImage(2*TST02_NUM_ROWS, TST02_NUM_COLS),
+      "Tested pmPeaksInImage() on (%d, %d) image", 2*TST02_NUM_ROWS, TST02_NUM_COLS);
+    ok(test_pmPeaksInImage(TST02_NUM_ROWS, 2*TST02_NUM_COLS),
+      "Tested pmPeaksInImage() on (%d, %d) image", TST02_NUM_ROWS, 2*TST02_NUM_COLS);
+
+
+
+    // ----------------------------------------
+    // Test pmPeaksSubset()
+    // Calling pmPeaksSubset with NULL psList.  Should generate error.
+    {
+        psMemId id = psMemGetId();
+        psArray *outData = pmPeaksSubset(NULL, 0.0, psRegionSet(0, 0, 0, 0));
+        ok(outData == NULL, "pmPeaksSubset() returned a NULL with a NULL psArray input");
+        psFree(outData);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Set peaks in input image.  All even-column and even-row pixels are
+    // set non-zero, all other pixels are set to zero.
+    psImage *imgData = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+    psS32 numPeaksOrig = 0;
+    for (psS32 i = 0 ; i < NUM_ROWS ; i++) {
+        for (psS32 j = 0 ; j < NUM_COLS ; j++) {
+            if ((0 == i%2) && (0 == j%2)) {
+                imgData->data.F32[i][j] = (float) (i + 10);
+                numPeaksOrig++;
+            } else {
+                imgData->data.F32[i][j] = 0.0;
+            }
+        }
+    }
+    // Call pmPeaksSubset() with large maxValue and disjoint psRegion.
+    // Should not remove any peaks.
+    {
+        psMemId id = psMemGetId();
+        psArray *outData = pmPeaksInImage(imgData, 0.0);
+        psArray *outData2 = pmPeaksSubset(outData, PS_MAX_F32, psRegionSet(20, 20, 20, 20));
+        ok(outData2 != NULL && psMemCheckArray(outData),
+           "pmPeaksSubset() returned a non-NULL psArray (large maxValue)");
+        ok(outData2->n == numPeaksOrig,
+          "pmPeaksSubset() returned correct number of peaks (was %d, should be %d)", outData2->n+1, numPeaksOrig);
+        psFree(outData);
+        psFree(outData2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPeaksSubset() with small maxValue and disjoint psRegion.
+    // Should not remove any peaks.
+    {
+        psMemId id = psMemGetId();
+        psArray *outData = pmPeaksInImage(imgData, 0.0);
+        psArray *outData2 = pmPeaksSubset(outData, 0.0, psRegionSet(20, 20, 20, 20));
+        ok(outData2 != NULL && psMemCheckArray(outData),
+           "pmPeaksSubset() returned a non-NULL psArray (small maxValue)");
+        ok(outData2->n == 0,
+          "pmPeaksSubset() returned correct number of peaks (was %d, should be %d)", outData2->n, 0);
+        psFree(outData);
+        psFree(outData2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPeaksSubset() with large maxValue and enclosing psRegion.
+    // Should remove aall peaks.
+    {
+        psMemId id = psMemGetId();
+        psArray *outData = pmPeaksInImage(imgData, 0.0);
+        psRegion tmpRegion = psRegionSet(-PS_MAX_F32, PS_MAX_F32, -PS_MAX_F32, PS_MAX_F32);
+        psArray *outData2 = pmPeaksSubset(outData, PS_MAX_F32, tmpRegion);
+        ok(outData2 != NULL && psMemCheckArray(outData),
+           "pmPeaksSubset() returned a non-NULL psArray (small maxValue, enclosing psRegion)");
+        ok(outData2->n == 0,
+          "pmPeaksSubset() returned correct number of peaks (was %d, should be %d)", outData2->n, 0);
+        psFree(outData);
+        psFree(outData2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Call pmPeaksSubset() with large maxValue and enclosing psRegion.
+    // Should remove aall peaks.
+    {
+        psMemId id = psMemGetId();
+        psArray *outData = pmPeaksInImage(imgData, 0.0);
+        psRegion tmpRegion = psRegionSet(-PS_MAX_F32, PS_MAX_F32, -PS_MAX_F32, PS_MAX_F32);
+        psArray *outData2 = pmPeaksSubset(outData, 0.0, tmpRegion);
+        ok(outData2 != NULL && psMemCheckArray(outData),
+           "pmPeaksSubset() returned a non-NULL psArray (small maxValue, inclusive psRegion)");
+        ok(outData2->n == 0,
+          "pmPeaksSubset() returned correct number of peaks (was %d, should be %d)", outData2->n, 0);
+        psFree(outData);
+        psFree(outData2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------
+    // Calling pmPeaksCompareAscend with NULL peak1
+    // XXX: This currently seg-faults because NULL args are not pretested in pmPeaksCompareAscend()
+    if (0) {
+        psMemId id = psMemGetId();
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 3.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareAscend(NULL, (const void **) peak2);
+        ok(rc == -1, "pmPeaksCompareAscend() returned correct result (peak1 < peak2)");
+        psFree(*peak2);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksCompareAscend with NULL peak2
+    // XXX: This currently seg-faults because NULL args are not pretested in pmPeaksCompareAscend()
+    if (0) {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareAscend((const void **)peak1, NULL);
+        ok(rc == -1, "pmPeaksCompareAscend() returned correct result (peak1 < peak2)");
+        psFree(*peak1);
+        psFree(peak1);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksCompareAscend with NULL *peak1
+    // XXX: This currently seg-faults because NULL args are not pretested in pmPeaksCompareAscend()
+    if (0) {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 3.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareAscend((const void **)peak1, (const void **) peak2);
+        ok(rc == -1, "pmPeaksCompareAscend() returned correct result (peak1 < peak2)");
+        psFree(peak1);
+        psFree(*peak2);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksCompareAscend with NULL *peak2
+    // XXX: This currently seg-faults because NULL args are not pretested in pmPeaksCompareAscend()
+    if (0) {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        int rc = pmPeaksCompareAscend((const void **)peak1, (const void **) peak2);
+        ok(rc == -1, "pmPeaksCompareAscend() returned correct result (peak1 < peak2)");
+        psFree(*peak1);
+        psFree(peak1);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Calling pmPeaksCompareAscend with peak1 < peak2
+    {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 3.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareAscend((const void **)peak1, (const void **) peak2);
+        ok(rc == -1, "pmPeaksCompareAscend() returned correct result (peak1 < peak2)");
+        psFree(*peak1);
+        psFree(peak1);
+        psFree(*peak2);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksCompareAscend with peak1 > peak2
+    {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 3.0, PM_PEAK_LONE);
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareAscend((const void **)peak1, (const void **) peak2);
+        ok(rc == 1, "pmPeaksCompareAscend() returned correct result (peak1 > peak2)");
+        psFree(*peak1);
+        psFree(peak1);
+        psFree(*peak2);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksCompareAscend with peak1 == peak2
+    {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareAscend((const void **)peak1, (const void **) peak2);
+        ok(rc == 0, "pmPeaksCompareAscend() returned correct result (peak1 == peak2)", rc);
+        psFree(*peak1);
+        psFree(peak1);
+        psFree(*peak2);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // ----------------------------------------
+    // Calling pmPeaksCompareDescend with NULL peak1
+    // XXX: This currently seg-faults because NULL args are not pretested in pmPeaksCompareDescend()
+    if (0) {
+        psMemId id = psMemGetId();
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 3.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareDescend(NULL, (const void **) peak2);
+        ok(rc == -1, "pmPeaksCompareDescend() returned correct result (peak1 < peak2)");
+        psFree(*peak2);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksCompareDescend with NULL peak2
+    // XXX: This currently seg-faults because NULL args are not pretested in pmPeaksCompareDescend()
+    if (0) {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareDescend((const void **)peak1, NULL);
+        ok(rc == -1, "pmPeaksCompareDescend() returned correct result (peak1 < peak2)");
+        psFree(*peak1);
+        psFree(peak1);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksCompareDescend with NULL *peak1
+    // XXX: This currently seg-faults because NULL args are not pretested in pmPeaksCompareDescend()
+    if (0) {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 3.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareDescend((const void **)peak1, (const void **) peak2);
+        ok(rc == -1, "pmPeaksCompareDescend() returned correct result (peak1 < peak2)");
+        psFree(peak1);
+        psFree(*peak2);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksCompareDescend with NULL *peak2
+    // XXX: This currently seg-faults because NULL args are not pretested in pmPeaksCompareDescend()
+    if (0) {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        int rc = pmPeaksCompareDescend((const void **)peak1, (const void **) peak2);
+        ok(rc == -1, "pmPeaksCompareDescend() returned correct result (peak1 < peak2)");
+        psFree(*peak1);
+        psFree(peak1);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+    // Calling pmPeaksCompareDescend with peak1 < peak2
+    {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 3.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareDescend((const void **)peak1, (const void **) peak2);
+        ok(rc == 1, "pmPeaksCompareDescend() returned correct result (peak1 < peak2)");
+        psFree(*peak1);
+        psFree(peak1);
+        psFree(*peak2);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksCompareDescend with peak1 > peak2
+    {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 3.0, PM_PEAK_LONE);
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareDescend((const void **)peak1, (const void **) peak2);
+        ok(rc == -1, "pmPeaksCompareDescend() returned correct result (peak1 > peak2)");
+        psFree(*peak1);
+        psFree(peak1);
+        psFree(*peak2);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+
+
+    // Calling pmPeaksCompareDescend with peak1 == peak2
+    {
+        psMemId id = psMemGetId();
+        pmPeak **peak1 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak1 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        pmPeak **peak2 = (pmPeak **) psAlloc(sizeof(pmPeak *));
+        *peak2 = pmPeakAlloc(3, 4, 2.0, PM_PEAK_LONE);
+        int rc = pmPeaksCompareDescend((const void **)peak1, (const void **) peak2);
+        ok(rc == 0, "pmPeaksCompareDescend() returned correct result (peak1 == peak2)", rc);
+        psFree(*peak1);
+        psFree(peak1);
+        psFree(*peak2);
+        psFree(peak2);
+        ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    }
+//HERE
+}
+
Index: /branches/eam_branch_20071023/psModules/test/objects/tap_pmSourceFitModel.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/objects/tap_pmSourceFitModel.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/objects/tap_pmSourceFitModel.c	(revision 15358)
@@ -0,0 +1,191 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+bool fitModels (psRandom *seed, float flux, float radius, float sigma);
+bool fitModelFlux (psRandom *seed, float flux, float radius, float sigma);
+
+// tests to check accuracy of fitted models for a range of fit radii, sigma, and flux.
+// we generate a fake source, then fit the model to it.  the difference or fractional
+// difference between the true and fitted parameters is saved.
+// we run these tests 200 times each an examine the resulting distribution of deviations.
+// the tests fail if the stdevs are more than 2x the expected stdev based on poisson noise
+// ** is 2x too generous?
+int main (void)
+{
+    pmModelGroupInit ();
+    pmSourceFitModelInit (15, 0.01, 1.0, true);
+
+    // psTraceSetLevel ("psModules.objects.pmSourceFitModel", 10);
+    // psTraceSetLevel ("psLib.math.psMinimizeLMChi2", 10);
+
+    plan_tests(240);
+
+    // build a gauss-deviate vector (mean = 0.0, sigma = 1.0)
+    psRandom *seed = psRandomAlloc (PS_RANDOM_TAUS, 0);
+
+    static float radius[] = {3.0, 5.0, 7.0, 10.0, 15.0, 25.0};
+    static float sigma[] = {1.0, 1.5, 2.0};
+    static float flux[] = {10000.0, 3000.0, 1000.0, 300.0, 100.0, 30.0, 10.0};
+
+    // drop-dead simple: should always work
+    bool status = fitModels (seed, 10000.0, 10.0, 2.0);
+    skip_start (!status, 240, "*** BASIC MODEL FITTING FAILS! *** : skipping related tests");
+
+    for (int i = 0; i < sizeof(sigma)/sizeof(float); i++) {
+        for (int j = 0; j < sizeof(radius)/sizeof(float); j++) {
+            for (int k = 0; k < sizeof(flux)/sizeof(float); k++) {
+                fitModels (seed, flux[k], radius[j], sigma[i]);
+            }
+        }
+    }
+
+    skip_end();
+    return exit_status();
+}
+
+static psVector *par1 = NULL;
+static psVector *par2 = NULL;
+static psVector *par3 = NULL;
+static psVector *par4 = NULL;
+static psVector *par5 = NULL;
+
+# define NMODELS 200
+bool fitModels (psRandom *seed, float flux, float radius, float sigma)
+{
+
+    psMemId id = psMemGetId();
+
+    diag("test model fit - flux: %f, radius: %f, sigma: %f", flux, radius, sigma);
+
+    par1 = psVectorAllocEmpty (NMODELS, PS_TYPE_F32);
+    par2 = psVectorAllocEmpty (NMODELS, PS_TYPE_F32);
+    par3 = psVectorAllocEmpty (NMODELS, PS_TYPE_F32);
+    par4 = psVectorAllocEmpty (NMODELS, PS_TYPE_F32);
+    par5 = psVectorAllocEmpty (NMODELS, PS_TYPE_F32);
+
+    for (int i = 0; i < NMODELS; i++) {
+        fitModelFlux (seed, flux, radius, sigma);
+    }
+
+    float signal = 2*M_PI*sigma*sigma*flux;
+    float noise = sqrt(signal + 4*M_PI*sigma*sigma*(100 + PS_SQR(5)));
+    float dMag = noise / signal;
+    float dPos = sigma * dMag;
+    diag ("signal: %f, noise: %f, dMag: %f, dPos: %f", signal, noise, dMag, dPos);
+
+    bool status = (par1->n == NMODELS);
+    ok (status, "all %d tests passed", NMODELS);
+
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+    psVectorStats (stats, par1, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dMag < 2.0), "Io ref/fit stdev: %e : %e sigma", stats->sampleStdev, stats->sampleStdev/dMag);
+    psVectorStats (stats, par2, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dPos < 2.0), "Xo ref/fit stdev: %e : %e sigma", stats->sampleStdev, stats->sampleStdev/dPos);
+    psVectorStats (stats, par3, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dPos < 2.0), "Yo ref/fit stdev: %e : %e sigma", stats->sampleStdev, stats->sampleStdev/dPos);
+    psVectorStats (stats, par4, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dMag < 2.0), "Sx ref/fit stdev: %e : %e sigma", stats->sampleStdev, stats->sampleStdev/dMag);
+    psVectorStats (stats, par5, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dMag < 2.0), "Sy ref/fit stdev: %e : %e sigma", stats->sampleStdev, stats->sampleStdev/dMag);
+
+    psFree (par1);
+    psFree (par2);
+    psFree (par3);
+    psFree (par4);
+    psFree (par5);
+    psFree (stats);
+
+    ok(!psMemCheckLeaks (id, NULL, stderr, false), "no memory leaks");
+    return status;
+}
+
+bool fitModelFlux (psRandom *seed, float flux, float radius, float sigma)
+{
+
+    psVector *rnd = psVectorAlloc (1000, PS_TYPE_F32);
+    for (int i = 0; i < rnd->n; i++) {
+        rnd->data.F32[i] = psRandomGaussian (seed);
+    }
+
+    // construct a model
+    pmSource *source = pmSourceAlloc ();
+    source->moments = pmMomentsAlloc ();
+
+    pmModelType type = pmModelClassGetType ("PS_MODEL_GAUSS");
+    source->modelEXT = pmModelAlloc (type);
+
+    source->modelEXT->params->data.F32[0] = 0;
+    source->modelEXT->params->data.F32[1] = flux;
+    source->modelEXT->params->data.F32[2] = 50;
+    source->modelEXT->params->data.F32[3] = 50;
+    source->modelEXT->params->data.F32[4] = 2.0*sqrt(sigma);
+    source->modelEXT->params->data.F32[5] = 2.0*sqrt(sigma);
+    source->modelEXT->params->data.F32[6] = 0;
+
+    source->pixels = psImageAlloc (100, 100, PS_TYPE_F32);
+    source->weight = psImageAlloc (100, 100, PS_TYPE_F32);
+    source->mask   = psImageAlloc (100, 100, PS_TYPE_U8);
+    psImageInit (source->pixels, 0.0);
+    psImageInit (source->weight, 0.0);
+    psImageInit (source->mask, 0);
+
+    // create an image with the model, and add noise: gain is 1, subtracted sky is 100, readnoise is 5
+    pmModelAdd (source->pixels, source->mask, source->modelEXT, PM_MODEL_OP_FULL);
+    int npix = 0;
+    for (int j = 0; j < source->pixels->numRows; j++) {
+        for (int i = 0; i < source->pixels->numCols; i++) {
+            float flux = source->pixels->data.F32[j][i];
+            float var = flux + 100 + PS_SQR(5);
+            source->pixels->data.F32[j][i] += rnd->data.F32[npix]*sqrt(var);
+            source->weight->data.F32[j][i] = var;
+            npix ++;
+            if (npix == rnd->n)
+                npix = 0;
+        }
+    }
+
+    // psFits *fits = psFitsOpen ("test.fits", "w");
+    // psFitsWriteImage (fits, NULL, source->pixels, 0, NULL);
+    // psFitsClose (fits);
+
+    // save the original model, modify params
+    pmModel *guess = pmModelCopy (source->modelEXT);
+    guess->params->data.F32[1] *= 0.9;
+    guess->params->data.F32[2] += 1.0;
+    guess->params->data.F32[3] -= 1.0;
+    guess->params->data.F32[4] *= 0.9;
+    guess->params->data.F32[5] *= 0.9;
+
+    psImageKeepCircle (source->mask, 50, 50, radius, "OR", PM_MASK_MARK);
+    bool status = pmSourceFitModel (source, guess, PM_SOURCE_FIT_EXT);
+    if (!status) {
+        psFree (rnd);
+        psFree (source);
+        psFree (guess);
+        return false;
+    }
+    psImageKeepCircle (source->mask, 50, 50, radius, "AND", PS_NOT_U8(PM_MASK_MARK));
+
+    par1->data.F32[par1->n] = (source->modelEXT->params->data.F32[1] / guess->params->data.F32[1]);
+    par2->data.F32[par2->n] = (source->modelEXT->params->data.F32[2] - guess->params->data.F32[2]);
+    par3->data.F32[par3->n] = (source->modelEXT->params->data.F32[3] - guess->params->data.F32[3]);
+    par4->data.F32[par4->n] = (source->modelEXT->params->data.F32[4] / guess->params->data.F32[4]);
+    par5->data.F32[par5->n] = (source->modelEXT->params->data.F32[5] / guess->params->data.F32[5]);
+
+    psVectorExtend (par1, 100, 1);
+    psVectorExtend (par2, 100, 1);
+    psVectorExtend (par3, 100, 1);
+    psVectorExtend (par4, 100, 1);
+    psVectorExtend (par5, 100, 1);
+
+    psFree (rnd);
+    psFree (source);
+    psFree (guess);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/test/objects/tap_pmSourceFitModel_Delta.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/objects/tap_pmSourceFitModel_Delta.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/objects/tap_pmSourceFitModel_Delta.c	(revision 15358)
@@ -0,0 +1,238 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+bool fitModels (psRandom *seed, float flux, float radius, float sigma);
+bool fitModelFlux (psRandom *seed, float flux, float radius, float sigma);
+bool printDev (float *src, float *par, int Npar, bool absolute);
+
+int main (void)
+{
+    pmModelGroupInit ();
+    pmSourceFitModelInit (15, 0.01, 1.0, true);
+
+    float flux = 10000;
+    float sigma = 2.0;
+    float radius = 10.0;
+
+    plan_tests(240);
+
+    // build a gauss-deviate vector (mean = 0.0, sigma = 1.0)
+    psRandom *seed = psRandomAlloc (PS_RANDOM_TAUS, 0);
+
+    // noise vector to noise up the image
+    psVector *rnd = psVectorAlloc (1000, PS_TYPE_F32);
+    for (int i = 0; i < rnd->n; i++) {
+        rnd->data.F32[i] = psRandomGaussian (seed);
+    }
+
+    // construct a GAUSS model
+    pmSource *source = pmSourceAlloc ();
+    source->moments = pmMomentsAlloc ();
+
+    pmModelType type = pmModelClassGetType ("PS_MODEL_GAUSS");
+    source->modelEXT = pmModelAlloc (type);
+
+    source->modelEXT->params->data.F32[0] = 0;
+    source->modelEXT->params->data.F32[1] = flux;
+    source->modelEXT->params->data.F32[2] = 50;
+    source->modelEXT->params->data.F32[3] = 50;
+    source->modelEXT->params->data.F32[4] = 2.0*sqrt(sigma);
+    source->modelEXT->params->data.F32[5] = 2.0*sqrt(sigma);
+    source->modelEXT->params->data.F32[6] = 0;
+
+    source->pixels = psImageAlloc (100, 100, PS_TYPE_F32);
+    source->weight = psImageAlloc (100, 100, PS_TYPE_F32);
+    source->mask   = psImageAlloc (100, 100, PS_TYPE_U8);
+    psImageInit (source->pixels, 0.0);
+    psImageInit (source->weight, 0.0);
+    psImageInit (source->mask, 0);
+
+    // create an image with the model, and add noise: gain is 1, subtracted sky is 100, readnoise is 5
+    pmModelAdd (source->pixels, source->mask, source->modelEXT, PM_MODEL_OP_FULL);
+    int npix = 0;
+    for (int j = 0; j < source->pixels->numRows; j++) {
+        for (int i = 0; i < source->pixels->numCols; i++) {
+            float flux = source->pixels->data.F32[j][i];
+            float var = flux + 100 + PS_SQR(5);
+            source->pixels->data.F32[j][i] += rnd->data.F32[npix]*sqrt(var);
+            source->weight->data.F32[j][i] = var;
+            npix ++;
+            if (npix == rnd->n)
+                npix = 0;
+        }
+    }
+
+    // fit with a PGAUSS model
+    pmModel *guess;
+
+    type = pmModelClassGetType ("PS_MODEL_PGAUSS");
+    guess = pmModelAlloc (type);
+
+    guess->params->data.F32[0] = 0;
+    guess->params->data.F32[1] = flux;
+    guess->params->data.F32[2] = 50;
+    guess->params->data.F32[3] = 50;
+    guess->params->data.F32[4] = 2.0*sqrt(sigma);
+    guess->params->data.F32[5] = 2.0*sqrt(sigma);
+    guess->params->data.F32[6] = 0;
+    // modify guess
+    guess->params->data.F32[1] *= 0.9;
+    guess->params->data.F32[2] += 1.0;
+    guess->params->data.F32[3] -= 1.0;
+    guess->params->data.F32[4] *= 0.9;
+    guess->params->data.F32[5] *= 0.9;
+
+    psImageKeepCircle (source->mask, 50, 50, radius, "OR", PM_MASK_MARK);
+    pmSourceFitModel (source, guess, PM_SOURCE_FIT_EXT);
+    psImageKeepCircle (source->mask, 50, 50, radius, "AND", PS_NOT_U8(PM_MASK_MARK));
+
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 1, false);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 2, true);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 3, true);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 4, false);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 5, false);
+
+    psImageKeepCircle (source->mask, 50, 50, radius, "OR", PM_MASK_MARK);
+    pmSourceFitModel (source, guess, PM_SOURCE_FIT_PSF);
+    psImageKeepCircle (source->mask, 50, 50, radius, "AND", PS_NOT_U8(PM_MASK_MARK));
+
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 1, false);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 2, true);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 3, true);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 4, false);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 5, false);
+
+    // muck up the Sx, Sy terms a little : how does this affect the dparams?
+    float Sx = guess->params->data.F32[4];
+    float Sy = guess->params->data.F32[5];
+    guess->params->data.F32[4] = 0.95*Sx;
+    guess->params->data.F32[5] = 0.95*Sy;
+
+    psImageKeepCircle (source->mask, 50, 50, radius, "OR", PM_MASK_MARK);
+    pmSourceFitModel (source, guess, PM_SOURCE_FIT_PSF);
+    psImageKeepCircle (source->mask, 50, 50, radius, "AND", PS_NOT_U8(PM_MASK_MARK));
+
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 1, false);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 2, true);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 3, true);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 4, false);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 5, false);
+
+    // muck up the Sx, Sy terms a little : how does this affect the dparams?
+    guess->params->data.F32[4] = 0.99*Sx;
+    guess->params->data.F32[5] = 0.99*Sy;
+
+    psImageKeepCircle (source->mask, 50, 50, radius, "OR", PM_MASK_MARK);
+    pmSourceFitModel (source, guess, PM_SOURCE_FIT_PSF);
+    psImageKeepCircle (source->mask, 50, 50, radius, "AND", PS_NOT_U8(PM_MASK_MARK));
+
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 1, false);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 2, true);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 3, true);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 4, false);
+    printDev (source->modelEXT->params->data.F32, guess->params->data.F32, 5, false);
+
+    psFree (rnd);
+    psFree (source);
+    psFree (guess);
+
+    return true;
+}
+
+bool printDev (float *src, float *fit, int Npar, bool absolute)
+{
+    float dev;
+    if (absolute) {
+        dev = (src[Npar]-fit[Npar]);
+        fprintf (stderr, "par %d : %f vs %f : abso dev %f\n", Npar, src[Npar], fit[Npar], dev);
+    } else {
+        dev = (src[Npar]/fit[Npar]);
+        fprintf (stderr, "par %d : %f vs %f : frac dev %f\n", Npar, src[Npar], fit[Npar], dev);
+    }
+    return true;
+}
+
+# if (0)
+    int main (void)
+{
+    pmModelGroupInit ();
+    pmSourceFitModelInit (15, 0.01, 1.0, true);
+
+    plan_tests(240);
+
+    // build a gauss-deviate vector (mean = 0.0, sigma = 1.0)
+    psRandom *seed = psRandomAlloc (PS_RANDOM_TAUS, 0);
+
+    static float radius[] = {3.0, 5.0, 7.0, 10.0, 15.0, 25.0};
+    static float sigma[] = {1.0, 1.5, 2.0};
+    static float flux[] = {10000.0, 3000.0, 1000.0, 300.0, 100.0, 30.0, 10.0};
+
+    for (int i = 0; i < sizeof(sigma)/sizeof(float); i++) {
+        for (int j = 0; j < sizeof(radius)/sizeof(float); j++) {
+            for (int k = 0; k < sizeof(flux)/sizeof(float); k++) {
+                fitModels (seed, flux[k], radius[j], sigma[i]);
+            }
+        }
+    }
+
+    return exit_status();
+}
+
+static psVector *par1 = NULL;
+static psVector *par2 = NULL;
+static psVector *par3 = NULL;
+static psVector *par4 = NULL;
+static psVector *par5 = NULL;
+
+bool fitModels (psRandom *seed, float flux, float radius, float sigma)
+{
+
+    psMemId id = psMemGetId();
+
+    diag("test model fit - flux: %f, radius: %f, sigma: %f", flux, radius, sigma);
+
+    par1 = psVectorAllocEmpty (200, PS_TYPE_F32);
+    par2 = psVectorAllocEmpty (200, PS_TYPE_F32);
+    par3 = psVectorAllocEmpty (200, PS_TYPE_F32);
+    par4 = psVectorAllocEmpty (200, PS_TYPE_F32);
+    par5 = psVectorAllocEmpty (200, PS_TYPE_F32);
+
+    for (int i = 0; i < 200; i++) {
+        fitModelFlux (seed, flux, radius, sigma);
+    }
+
+    float signal = 2*M_PI*sigma*sigma*flux;
+    float noise = sqrt(signal + 4*M_PI*sigma*sigma*(100 + PS_SQR(5)));
+    float dMag = noise / signal;
+    float dPos = sigma * dMag;
+    diag ("signal: %f, noise: %f, dMag: %f, dPos: %f", signal, noise, dMag, dPos);
+
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+    psVectorStats (stats, par1, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dMag < 2.0), "Io ref/fit stdev: %f : %f sigma", stats->sampleStdev, stats->sampleStdev/dMag);
+    psVectorStats (stats, par2, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dPos < 2.0), "Xo ref/fit stdev: %f : %f sigma", stats->sampleStdev, stats->sampleStdev/dPos);
+    psVectorStats (stats, par3, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dPos < 2.0), "Yo ref/fit stdev: %f : %f sigma", stats->sampleStdev, stats->sampleStdev/dPos);
+    psVectorStats (stats, par4, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dMag < 2.0), "Sx ref/fit stdev: %f : %f sigma", stats->sampleStdev, stats->sampleStdev/dMag);
+    psVectorStats (stats, par5, NULL, NULL, 0);
+    ok ((stats->sampleStdev/dMag < 2.0), "Sy ref/fit stdev: %f : %f sigma", stats->sampleStdev, stats->sampleStdev/dMag);
+
+    psFree (par1);
+    psFree (par2);
+    psFree (par3);
+    psFree (par4);
+    psFree (par5);
+    psFree (stats);
+
+    ok(!psMemCheckLeaks (id, NULL, NULL, false), "no memory leaks");
+    return true;
+}
+
+# endif
Index: /branches/eam_branch_20071023/psModules/test/objects/tap_pmSourcePhotometry.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/objects/tap_pmSourcePhotometry.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/objects/tap_pmSourcePhotometry.c	(revision 15358)
@@ -0,0 +1,167 @@
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+
+#include "tap.h"
+#include "pstap.h"
+
+bool pmSourcePhotometry_TestOffsets (double radius, double sigma, double fitMag, double apMag, double err1, double err2);
+
+int main (void)
+{
+
+    pmModelGroupInit ();
+
+    plan_tests(240);
+
+    diag("pmSourcePhotometry tests");
+
+    // test consistency of interpolated photometry for a range of apertures (for a fixed PSF sigma)
+    pmSourcePhotometry_TestOffsets (15.0, 2.0, -10.3759, -10.3759, +0.0000, +0.0000);
+    pmSourcePhotometry_TestOffsets (10.0, 2.0, -10.3759, -10.3759, +0.0000, +0.0000);
+    pmSourcePhotometry_TestOffsets ( 8.0, 2.0, -10.3759, -10.3759, +0.0000, +0.0000);
+    pmSourcePhotometry_TestOffsets ( 7.0, 2.0, -10.3759, -10.3759, +0.0000, +0.0001);
+    pmSourcePhotometry_TestOffsets ( 6.0, 2.0, -10.3759, -10.3758, +0.0001, +0.0004);
+    pmSourcePhotometry_TestOffsets ( 5.0, 2.0, -10.3759, -10.3733, +0.0003, +0.0011);
+    pmSourcePhotometry_TestOffsets ( 4.0, 2.0, -10.3759, -10.3520, +0.0006, +0.0018);
+    pmSourcePhotometry_TestOffsets ( 3.0, 2.0, -10.3759, -10.2626, +0.0001, +0.0002);
+    pmSourcePhotometry_TestOffsets ( 2.0, 2.0, -10.3759,  -9.7729, -0.0027, -0.0089);
+    pmSourcePhotometry_TestOffsets ( 1.0, 2.0, -10.3759,  -8.8689, -0.0051, -0.0161);
+
+    // test consistency of interpolated photometry for a range of apertures (for a fixed PSF sigma)
+    pmSourcePhotometry_TestOffsets (15.0, 1.5, -10.3759, -10.3759, +0.0000, +0.0000);
+    pmSourcePhotometry_TestOffsets (10.0, 1.5, -10.3759, -10.3759, +0.0000, +0.0000);
+    pmSourcePhotometry_TestOffsets ( 8.0, 1.5, -10.3759, -10.3759, +0.0000, +0.0000);
+    pmSourcePhotometry_TestOffsets ( 7.0, 1.5, -10.3759, -10.3759, +0.0000, +0.0001);
+    pmSourcePhotometry_TestOffsets ( 6.0, 1.5, -10.3759, -10.3758, +0.0001, +0.0004);
+    pmSourcePhotometry_TestOffsets ( 5.0, 1.5, -10.3759, -10.3733, +0.0003, +0.0011);
+    pmSourcePhotometry_TestOffsets ( 4.0, 1.5, -10.3759, -10.3520, +0.0006, +0.0018);
+    pmSourcePhotometry_TestOffsets ( 3.0, 1.5, -10.3759, -10.2626, +0.0001, +0.0002);
+    pmSourcePhotometry_TestOffsets ( 2.0, 1.5, -10.3759,  -9.7729, -0.0027, -0.0089);
+    pmSourcePhotometry_TestOffsets ( 1.0, 1.5, -10.3759,  -8.8689, -0.0051, -0.0161);
+
+    // test consistency of interpolated photometry for a range of apertures (for a fixed PSF sigma)
+    pmSourcePhotometry_TestOffsets (15.0, 1.0,  -9.4955,  -9.4955, +0.0000, +0.0000);
+    pmSourcePhotometry_TestOffsets (10.0, 1.0,  -9.4955,  -9.4955, +0.0000, +0.0000);
+    pmSourcePhotometry_TestOffsets ( 8.0, 1.0,  -9.4955,  -9.4955, +0.0000, +0.0000);
+    pmSourcePhotometry_TestOffsets ( 7.0, 1.0,  -9.4955,  -9.4955, +0.0000, +0.0000);
+    pmSourcePhotometry_TestOffsets ( 6.0, 1.0,  -9.4955,  -9.4955, +0.0000, +0.0001);
+    pmSourcePhotometry_TestOffsets ( 5.0, 1.0,  -9.4955,  -9.4955, +0.0001, +0.0002);
+    pmSourcePhotometry_TestOffsets ( 4.0, 1.0,  -9.4955,  -9.4968, +0.0006, +0.0022);
+    pmSourcePhotometry_TestOffsets ( 3.0, 1.0,  -9.4955,  -9.4945, +0.0021, +0.0068);
+    pmSourcePhotometry_TestOffsets ( 2.0, 1.0,  -9.4955,  -9.3323, -0.0034, -0.0118);
+    pmSourcePhotometry_TestOffsets ( 1.0, 1.0,  -9.4955,  -8.6844, -0.0141, -0.0440);
+
+    return exit_status();
+}
+
+bool pmSourcePhotometry_TestOffsets (double radius, double sigma, double fitMag, double apMag, double err1, double err2)
+{
+    psMemId id = psMemGetId();
+
+    diag("pmSourcePhotometry test offsets for radius %f", radius);
+
+    // generate a simple readout
+    pmReadout *readout = pmReadoutAlloc (NULL);
+    skip_start(readout == NULL, 0, "Skipping tests because pmReadoutAlloc failed");
+
+    readout->image = psImageAlloc (64, 64, PS_TYPE_F32);
+    readout->mask  = psImageAlloc (64, 64, PS_TYPE_U8);
+
+    // create an empty reference image
+    psImageInit (readout->image, 0.0);
+    psImageInit (readout->mask, 0);
+
+    // generate a simple psf
+    pmPSF *psf = pmPSFBuildSimple ("PS_MODEL_GAUSS", sigma, sigma, 0.0);
+    // psf->growth = pmGrowthCurveAlloc (2.0, 100.0, 15.0);
+    // pmGrowthCurveGenerate (readout, psf, false);
+
+    // create a source
+    pmSource *source = pmSourceAlloc ();
+    source->pixels = psMemIncrRefCounter (readout->image);
+    source->mask   = psMemIncrRefCounter (readout->mask);
+    source->type   = PM_SOURCE_TYPE_STAR;
+    source->mode   = PM_SOURCE_MODE_SUBTRACTED;
+
+    // create template model and measure apMag at fractional offsets
+    pmModel *modelRef = pmModelAlloc(psf->type);
+    modelRef->params->data.F32[PM_PAR_SKY] = 0;
+    modelRef->params->data.F32[PM_PAR_I0] = 1000;
+    modelRef->params->data.F32[PM_PAR_XPOS] = 32.5;
+    modelRef->params->data.F32[PM_PAR_YPOS] = 32.5;
+
+    // create modelPSF from this model
+    source->modelPSF = pmModelFromPSF (modelRef, psf);
+    source->modelPSF->dparams->data.F32[PM_PAR_I0] = 1;
+    source->modelPSF->radiusFit = radius;
+
+    // measure photometry for centered source (fractional pix : 0.5,0.5)
+    // pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_GROWTH | PM_SOURCE_PHOT_INTERP);
+    pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_INTERP);
+    ok_float_tol(source->psfMag, fitMag, 0.0002, "source fitMag is %f", source->psfMag);
+    ok_float_tol(source->apMag,  apMag, 0.0002, "source apMag is %f", source->apMag);
+    ok_float(source->errMag,   0.001, "source errMag is %f", source->errMag);
+    float refMag = source->apMag;
+
+    // these use an offset of 0.2,0.2
+    // measure photometry for a sub-pixel offset position
+    source->modelPSF->params->data.F32[PM_PAR_XPOS] = 32.3;
+    source->modelPSF->params->data.F32[PM_PAR_YPOS] = 32.3;
+    pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_INTERP);
+    ok_float_tol(refMag - source->apMag,  err1, 0.0002, "offset error is %f", refMag - source->apMag);
+
+    // measure photometry for a sub-pixel offset position
+    source->modelPSF->params->data.F32[PM_PAR_XPOS] = 32.7;
+    source->modelPSF->params->data.F32[PM_PAR_YPOS] = 32.3;
+    pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_INTERP);
+    ok_float_tol(refMag - source->apMag,  err1, 0.0002, "offset error is %f", refMag - source->apMag);
+
+    // measure photometry for a sub-pixel offset position
+    source->modelPSF->params->data.F32[PM_PAR_XPOS] = 32.3;
+    source->modelPSF->params->data.F32[PM_PAR_YPOS] = 32.7;
+    pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_INTERP);
+    ok_float_tol(refMag - source->apMag,  err1, 0.0002, "offset error is %f", refMag - source->apMag);
+
+    // measure photometry for a sub-pixel offset position
+    source->modelPSF->params->data.F32[PM_PAR_XPOS] = 32.7;
+    source->modelPSF->params->data.F32[PM_PAR_YPOS] = 32.7;
+    pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_INTERP);
+    ok_float_tol(refMag - source->apMag,  err1, 0.0002, "offset error is %f", refMag - source->apMag);
+
+    // these use an offset of 0.4,0.4
+    // measure photometry for a sub-pixel offset position
+    source->modelPSF->params->data.F32[PM_PAR_XPOS] = 32.1;
+    source->modelPSF->params->data.F32[PM_PAR_YPOS] = 32.1;
+    pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_INTERP);
+    ok_float_tol(refMag - source->apMag,  err2, 0.0002, "offset error is %f", refMag - source->apMag);
+
+    // measure photometry for a sub-pixel offset position
+    source->modelPSF->params->data.F32[PM_PAR_XPOS] = 32.9;
+    source->modelPSF->params->data.F32[PM_PAR_YPOS] = 32.1;
+    pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_INTERP);
+    ok_float_tol(refMag - source->apMag,  err2, 0.0002, "offset error is %f", refMag - source->apMag);
+
+    // measure photometry for a sub-pixel offset position
+    source->modelPSF->params->data.F32[PM_PAR_XPOS] = 32.1;
+    source->modelPSF->params->data.F32[PM_PAR_YPOS] = 32.9;
+    pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_INTERP);
+    ok_float_tol(refMag - source->apMag,  err2, 0.0002, "offset error is %f", refMag - source->apMag);
+
+    // measure photometry for a sub-pixel offset position
+    source->modelPSF->params->data.F32[PM_PAR_XPOS] = 32.9;
+    source->modelPSF->params->data.F32[PM_PAR_YPOS] = 32.9;
+    pmSourceMagnitudes (source, psf, PM_SOURCE_PHOT_INTERP);
+    ok_float_tol(refMag - source->apMag,  err2, 0.0002, "offset error is %f", refMag - source->apMag);
+
+    psFree (source);
+    psFree (modelRef);
+    psFree (psf);
+    psFree (readout);
+
+    skip_end();
+
+    ok(!psMemCheckLeaks (id, NULL, stdout, false), "no memory leaks");
+    return true;
+}
Index: /branches/eam_branch_20071023/psModules/test/objects/tst_pmObjects01.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/objects/tst_pmObjects01.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/objects/tst_pmObjects01.c	(revision 15358)
@@ -0,0 +1,1300 @@
+/** @file tst_pmObjects.c
+ *
+ *  @brief Contains the tests for pmObjects.c:
+ *
+ * test00: This code will test the pmObjects routines.
+ *
+ *  @author GLG, MHPCC
+ *
+ * XXX: Must test
+ *       pmSourceRoughClass
+ *  many others...
+ *
+ *
+ * XXX: Must test output results for many other functions.
+ *
+ * XXX: There are many cases where row/col can be switched in the code.
+ * We must test that here by using non-square images.  All tests
+ * in this file should be run with non-square images.
+ *
+ * XXX: Memory leaks are not being caught.  If I allocated a psVector in these functions
+ * and never deallocate, no error is generated.
+ *
+ * XXX: Much of this file is commented out due to the API changes in rel 7.
+ *
+ *
+Fully Tested:
+    pmPeakAlloc()
+    pmMomentsAlloc()
+Weakly Tested:
+    pmSourceMoments()
+    most of psObjects.c is not tested
+ *
+ *  @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-08-24 00:11:59 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+#include "psTest.h"
+#include "pslib.h"
+#include "pmObjects.h"
+#include "pmModelGroup.h"
+
+#define NUM_ROWS 10
+#define NUM_COLS 10
+#define ERROR_TOLERANCE 1.0
+#define ERROR_TOL 0.001
+static int test00(void);
+static int test01(void);
+static int test02(void);
+static int test03(void);
+static int test04(void);
+static int test05(void);
+//static int test06(void);
+//static int test07(void);
+//static int test08(void);
+//static int test09(void);
+//static int test15(void);
+//static int test16(void);
+//static int test20(void);
+testDescription tests[] = {
+                              {test00, 000, "pmObjects: structure allocators and deallocators", true, false},
+                              {test01, 001, "pmObjects: psFindVectorPeaks()", true, false},
+                              {test02, 001, "pmObjects: psFindImagePeaks()", true, false},
+                              {test03, 001, "pmObjects: pmCullPeaks()", true, false},
+                              {test04, 001, "pmObjects: pmSourceLocalSky()", true, false},
+                              {test05, 001, "pmObjects: pmSourceMoments()", true, false},
+                              //                              {test06, 001, "pmObjects: pmSourceSetPixelsCircle()", true, false},
+                              //                              {test07, 001, "pmObjects: pmMin()", true, false},
+                              //                              {test08, 001, "pmObjects: pmSourceModelGuess()", true, false},
+                              //{test09, 001, "pmObjects: pmSourceContour()", true, false},
+                              //{test15, 001, "pmObjects: pmSourceAddModel()", true, false},
+                              //{test16, 001, "pmObjects: pmSourceSubModel()", true, false},
+                              //{test20, 001, "pmObjects: pmSourceSubModel()", true, false},
+                              {NULL}
+                          };
+
+int main(int argc, char* argv[])
+{
+    psLogSetFormat("HLNM");
+    //
+    // We include the function names here in psTraceSetLevel() commands for
+    // debugging convenience.  There is no guarantee that this list of functions
+    // is complete.
+    //
+    psTraceSetLevel(".", 0);
+    psTraceSetLevel("pmPeakAlloc", 0);
+    psTraceSetLevel("pmMomentsAlloc", 0);
+    psTraceSetLevel("modelFree", 0);
+    psTraceSetLevel("pmModelAlloc", 0);
+    psTraceSetLevel("sourceFree", 0);
+    psTraceSetLevel("pmSourceAlloc", 0);
+    psTraceSetLevel("pmFindVectorPeaks", 0);
+    psTraceSetLevel("getRowVectorFromImage", 0);
+    psTraceSetLevel("myListAddPeak", 0);
+    psTraceSetLevel("pmFindImagePeaks", 0);
+    psTraceSetLevel("isItInThisRegion", 0);
+    psTraceSetLevel("pmCullPeaks", 0);
+    psTraceSetLevel("pmPeaksSubset", 0);
+    psTraceSetLevel("pmSourceLocalSky", 0);
+    psTraceSetLevel("checkRadius2", 0);
+    psTraceSetLevel("pmSourceMoments", 0);
+    psTraceSetLevel("pmComparePeakAscend", 0);
+    psTraceSetLevel("pmComparePeakDescend", 0);
+    psTraceSetLevel("pmSourcePSFClump", 0);
+    psTraceSetLevel("pmSourceRoughClass", 0);
+    psTraceSetLevel("pmSourceDefinePixels", 0);
+    psTraceSetLevel("pmSourceModelGuess", 0);
+    psTraceSetLevel("pmModelEval", 0);
+    psTraceSetLevel("findValue", 0);
+    psTraceSetLevel("pmSourceContour", 0);
+    psTraceSetLevel("pmSourceFitModel_v5", 0);
+    psTraceSetLevel("pmSourceFitModel", 0);
+    psTraceSetLevel("p_pmSourceAddOrSubModel", 0);
+    psTraceSetLevel("pmSourceAddModel", 0);
+    psTraceSetLevel("pmSourceSubModel", 0);
+
+    return !runTestSuite(stderr, "Test Point Driver", tests, argc, argv);
+}
+
+/******************************************************************************
+test00(): Test the various allocators and deallocators.
+ *****************************************************************************/
+int test00( void )
+{
+    bool testStatus = true;
+    psTraceSetLevel(".", 0);
+
+    printf("Testing pmPeakAlloc()...\n");
+    pmPeak *tmpPeak = pmPeakAlloc(1, 2, 3.0, PM_PEAK_LONE);
+    if (tmpPeak == NULL) {
+        printf("TEST ERROR: pmPeakAlloc() returned a NULL pmPeak\n");
+        testStatus = false;
+    } else {
+        if (tmpPeak->x != 1) {
+            printf("TEST ERROR: pmPeakAlloc() improperly set pmPeak->x\n");
+            testStatus = false;
+        }
+        if (tmpPeak->y != 2) {
+            printf("TEST ERROR: pmPeakAlloc() improperly set pmPeak->y\n");
+            testStatus = false;
+        }
+        if (tmpPeak->counts != 3.0) {
+            printf("TEST ERROR: pmPeakAlloc() improperly set pmPeak->counts\n");
+            testStatus = false;
+        }
+        if (tmpPeak->class != PM_PEAK_LONE) {
+            printf("TEST ERROR: pmPeakAlloc() improperly set pmPeak->class\n");
+            testStatus = false;
+        }
+    }
+    psFree(tmpPeak);
+
+    printf("Testing pmMomentsAlloc()...\n");
+    pmMoments *tmpMoments = pmMomentsAlloc();
+    if (tmpMoments == NULL) {
+        printf("TEST ERROR: pmMomentsAlloc() returned a NULL pmMoments\n");
+        testStatus = false;
+    } else {
+        if ((fabs(tmpMoments->x-0.0) > ERROR_TOL) ||
+                (fabs(tmpMoments->y-0.0) > ERROR_TOL) ||
+                (fabs(tmpMoments->Sx-0.0) > ERROR_TOL) ||
+                (fabs(tmpMoments->Sy-0.0) > ERROR_TOL) ||
+                (fabs(tmpMoments->Sxy-0.0) > ERROR_TOL) ||
+                (fabs(tmpMoments->Sum-0.0) > ERROR_TOL) ||
+                (fabs(tmpMoments->Peak-0.0) > ERROR_TOL) ||
+                (fabs(tmpMoments->Sky-0.0) > ERROR_TOL) ||
+                (tmpMoments->nPixels != 0)) {
+            printf("TEST ERROR: pmMomentsAlloc() did not properly initialize the pmMoments structure.\n");
+            printf("    tmpMoments->x is %f\n", tmpMoments->x);
+            printf("    tmpMoments->y is %f\n", tmpMoments->y);
+            printf("    tmpMoments->Sx is %f\n", tmpMoments->Sx);
+            printf("    tmpMoments->Sy is %f\n", tmpMoments->Sy);
+            printf("    tmpMoments->Sxy is %f\n", tmpMoments->Sxy);
+            printf("    tmpMoments->Sum is %f\n", tmpMoments->Sum);
+            printf("    tmpMoments->Peak is %f\n", tmpMoments->Peak);
+            printf("    tmpMoments->Sky is %f\n", tmpMoments->Sky);
+            printf("    tmp    Moments->nPixels is %d\n", tmpMoments->nPixels);
+            testStatus = false;
+        }
+    }
+    psFree(tmpMoments);
+
+
+    //
+    // Loop through each type of model
+    //
+    psS32 i = 0;
+    while (0 != pmModelClassParameterCount(i)) {
+        printf("Testing pmModelAlloc(%s)...\n", pmModelClassGetName(0));
+        pmModel *tmpModel = pmModelAlloc(i);
+        if (tmpModel == NULL) {
+            printf("TEST ERROR: pmModelAlloc(%s) returned a NULL pmModel\n", pmModelClassGetName(0));
+            testStatus = false;
+        } else {
+
+            /* XXX: Should we test that the members were set correctly?
+                        if ((tmpModel->params->n != 7) || (tmpModel->dparams->n != 7)) {
+                            printf("TEST ERROR: pmModelAlloc(PS_MODEL_GAUSS) allocated an incorrect number of params (%ld, %ld)\n",
+                                   tmpModel->params->n, tmpModel->dparams->n);
+                            testStatus = false;
+                        } else {
+                            for (psS32 i = 0 ; i < 7 ; i++) {
+                                if ((tmpModel->params->data.F32[i] != 0.0) ||
+                                        (tmpModel->dparams->data.F32[i] != 0.0)) {
+                                    printf("TEST ERROR: pmModelAlloc(PS_MODEL_GAUSS) did not ininitialize the params/dparams array to 0.0.\n");
+                                    testStatus = false;
+                                }
+                            }
+                        }
+            */
+        }
+        psFree(tmpModel);
+        i++;
+    }
+
+
+    pmSource *tmpSource = pmSourceAlloc();
+    if (tmpSource == NULL) {
+        printf("TEST ERROR: pmSourceAlloc() returned a NULL pmSource\n");
+        testStatus = false;
+    }
+    psFree(tmpSource);
+
+    return(testStatus);
+}
+
+/******************************************************************************
+test01(): we first test pmFindVectorPeaks() with a variety of bad input
+parameters.  Then we test it with a simple vector both 1- and multi-elements.
+ *****************************************************************************/
+#define TST01_VECTOR_LENGTH 10
+bool test_pmFindVectorPeaks(int n)
+{
+    bool testStatus = true;
+    psVector *inData = psVectorAlloc(n, PS_TYPE_F32);
+    inData->n = inData->nalloc;
+    psVector *outData = NULL;
+
+    printf("-------------- Calling test_pmFindVectorPeaks on an %d size vector. --------------\n", n);
+    //
+    // Test first pixel peak.
+    //
+    printf("Test pmFindVectorPeaks() with a first-element peak.\n");
+    for (psS32 i = 0 ; i < n ; i++) {
+        inData->data.F32[i] = (float) (n-i);
+    }
+    inData->data.F32[0] = (float) n;
+
+    outData= pmFindVectorPeaks(inData, 0.0);
+    if (outData == NULL) {
+        printf("TEST ERROR: pmFindVectorPeaks returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+        if (outData->n != 1) {
+            printf("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+        if (outData->data.U32[0] != 0) {
+            printf("TEST ERROR: Did not find peak at element 0.\n");
+            testStatus = false;
+        }
+        psFree(outData);
+    }
+
+    //
+    // Test first pixel peak, large threshold
+    //
+    printf("Test pmFindVectorPeaks() with a first-element peak, large threshold.\n");
+    for (psS32 i = 0 ; i < n ; i++) {
+        inData->data.F32[i] = (float) (n-i);
+    }
+    inData->data.F32[0] = (float) n;
+
+    outData= pmFindVectorPeaks(inData, (float) (n*n));
+    if (outData == NULL) {
+        printf("TEST ERROR: pmFindVectorPeaks returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+
+        if (outData->n != 0) {
+            printf("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+        psFree(outData);
+
+        // Skip remaining tests if the input vector has length 1.
+        if (n == 1) {
+            psFree(inData);
+            return(testStatus);
+        }
+    }
+
+    //
+    // Test last pixel peak.
+    //
+    printf("Test pmFindVectorPeaks() with a last-element peak.\n");
+    for (psS32 i = 0 ; i < n ; i++) {
+        inData->data.F32[i] = (float) (i);
+    }
+    inData->data.F32[n-1] = (float) n;
+
+    outData= pmFindVectorPeaks(inData, 0.0);
+    if (outData == NULL) {
+        printf("TEST ERROR: pmFindVectorPeaks returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+
+        if (outData->n != 1) {
+            printf("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+        if (outData->data.U32[0] != n-1) {
+            printf("TEST ERROR: Did not find peak at element %d.\n", n-1);
+            testStatus = false;
+        }
+        psFree(outData);
+    }
+
+    //
+    // Test last pixel peak, large threshold.
+    //
+    printf("Test pmFindVectorPeaks() with a last-element peak, large threshold.\n");
+    for (psS32 i = 0 ; i < n ; i++) {
+        inData->data.F32[i] = (float) (i);
+    }
+    inData->data.F32[n-1] = (float) n;
+
+    outData= pmFindVectorPeaks(inData, (float) (n*n));
+    if (outData == NULL) {
+        printf("TEST ERROR: pmFindVectorPeaks returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+
+        if (outData->n != 0) {
+            printf("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+        psFree(outData);
+    }
+
+    //
+    // Test interior peaks.
+    // Set all even number elements to be peaks.
+    //
+    printf("Test pmFindVectorPeaks() with all even-numbered elements peak.\n");
+    for (psS32 i = 0 ; i < n ; i++) {
+        if (0 == i%2) {
+            inData->data.F32[i] = (float) (2 * i);
+        } else {
+            inData->data.F32[i] = (float) (i);
+        }
+    }
+    inData->data.F32[0] = (float) n;
+
+
+    outData= pmFindVectorPeaks(inData, 0.0);
+    if (outData == NULL) {
+        printf("TEST ERROR: pmFindVectorPeaks returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+
+        if (outData->n != n/2) {
+            printf("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+
+        for (psS32 i = 0 ; i < outData->n ; i++) {
+            if (outData->data.U32[i] != (2 * i)) {
+                printf("TEST ERROR: the %d-th peak is element number %d\n", i, outData->data.U32[i]);
+                testStatus = false;
+            }
+        }
+        psFree(outData);
+    }
+
+    //
+    // Test interior peaks, with threshold = n*n.
+    // Should generate an empty output psVector.
+    //
+    printf("Test pmFindVectorPeaks() with all even-numbered elements peak, large threshold.\n");
+    outData= pmFindVectorPeaks(inData, (float) (n*n));
+    if (outData == NULL) {
+        printf("TEST ERROR: pmFindVectorPeaks returned a NULL psVector.\n");
+        testStatus = false;
+    } else {
+
+        if (outData->n != 0) {
+            printf("TEST ERROR: outData->n is %ld\n", outData->n);
+            testStatus = false;
+        }
+        psFree(outData);
+    }
+
+    psFree(inData);
+    return(testStatus);
+}
+
+int test01( void )
+{
+    bool testStatus = true;
+    psVector *tmpVec = NULL;
+    psVector *tmpVecF64 = psVectorAlloc(TST01_VECTOR_LENGTH, PS_TYPE_F64);
+    psVector *tmpVecEmpty = psVectorAlloc(0, PS_TYPE_F32);
+    tmpVecF64->n = tmpVecF64->nalloc;
+    tmpVecEmpty->n = tmpVecEmpty->nalloc;
+
+    psTraceSetLevel(".", 0);
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmFindVectorPeaks with NULL psVector.  Should generate error and return NULL.\n");
+    tmpVec = pmFindVectorPeaks(NULL, 0.0);
+    if (tmpVec != NULL) {
+        printf("TEST ERROR: pmFindVectorPeaks() returned a non-NULL psVector.\n");
+        testStatus = false;
+        psFree(tmpVec);
+    }
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmFindVectorPeaks with empty psVector.  Should generate error and return NULL.\n");
+    tmpVec = pmFindVectorPeaks(tmpVecEmpty, 0.0);
+    if (tmpVec != NULL) {
+        printf("TEST ERROR: pmFindVectorPeaks() returned a non-NULL psVector.\n");
+        testStatus = false;
+        psFree(tmpVec);
+    }
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmFindVectorPeaks with PS_TYPE_F64 psVector.  Should generate error and return NULL.\n");
+    tmpVec = pmFindVectorPeaks(tmpVecF64, 0.0);
+    if (tmpVec != NULL) {
+        printf("TEST ERROR: pmFindVectorPeaks() returned a non-NULL psVector.\n");
+        testStatus = false;
+        psFree(tmpVec);
+    }
+    testStatus&= test_pmFindVectorPeaks(1);
+    testStatus&= test_pmFindVectorPeaks(TST01_VECTOR_LENGTH);
+
+    psFree(tmpVecF64);
+    psFree(tmpVecEmpty);
+    return(testStatus);
+}
+
+/******************************************************************************
+test02():
+// XXX: Must test flat peaks.
+// XXX: test 1-by-n and n-by-1 images.
+ *****************************************************************************/
+#define TST02_NUM_ROWS 5
+#define TST02_NUM_COLS 5
+bool test_pmFindImagePeaks(int numRows, int numCols)
+{
+    printf("-------------- Calling test_pmFindImagePeaks on an %d-by-%d image. --------------\n", numRows, numCols);
+    //    if ((numRows < 4) || (numCols < 4)) {
+    //        printf("WARNING: Don't call this test with a smaller than 4-by-4 image.\n");
+    //        return(true);
+    //    }
+    bool testStatus = true;
+    psImage *inData = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    psArray *outData = NULL;
+
+    //
+    // Initialize test image.
+    //
+    for (psS32 i = 0 ; i < numRows ; i++) {
+        for (psS32 j = 0 ; j < numCols ; j++) {
+            inData->data.F32[i][j] = PS_SQR(i - numRows/2) + PS_SQR(j-numCols/2);
+        }
+    }
+    //
+    // Set corner and center pixels as peaks.
+    //
+    inData->data.F32[0][0] = PS_SQR(numRows) + PS_SQR(numCols);
+    inData->data.F32[0][numCols-1] = PS_SQR(numRows) + PS_SQR(numCols);
+    inData->data.F32[numRows-1][0] = PS_SQR(numRows) + PS_SQR(numCols);
+    inData->data.F32[numRows-1][numCols-1] = PS_SQR(numRows) + PS_SQR(numCols);
+    inData->data.F32[numRows/2][numCols/2] = PS_SQR(numRows) + PS_SQR(numCols);
+
+    //
+    // Print image.
+    //
+    for (psS32 i = 0 ; i < numRows ; i++) {
+        for (psS32 j = 0 ; j < numCols ; j++) {
+            printf("(%.1f) ", inData->data.F32[i][j]);
+        }
+        printf("\n");
+    }
+
+    //
+    // Call pmFindImagePeaks() with a threshold of 0.0.
+    //
+    outData = pmFindImagePeaks(inData, 0.0);
+
+    if (outData == NULL) {
+        printf("TEST ERROR: pmFindImagePeaks returned a NULL psList.\n");
+        testStatus = false;
+    } else {
+        psS32 expectedNumPeaks;
+        if ((numRows == 1) && (numCols == 1)) {
+            expectedNumPeaks = 1;
+        } else if ((numRows == 1) || (numCols == 1)) {
+            expectedNumPeaks = 3;
+        } else {
+            expectedNumPeaks = 5;
+        }
+        if (outData->n != expectedNumPeaks) {
+            printf("TEST ERROR: pmFindImagePeaks found %ld peaks (should be %d)\n", outData->n, expectedNumPeaks);
+            testStatus = false;
+        }
+
+        // HEY: verify
+        for (psS32 i = 0 ; i < outData->n ; i++) {
+            pmPeak *tmpPeak = (pmPeak *) outData->data[i];
+            if (((tmpPeak->x == 0) && (tmpPeak->y == 0)) ||
+                    ((tmpPeak->x == 0) && (tmpPeak->y == numRows-1)) ||
+                    ((tmpPeak->x == numCols-1) && (tmpPeak->y == 0)) ||
+                    ((tmpPeak->x == numCols-1) && (tmpPeak->y == numRows-1))) {
+                if (!((tmpPeak->class & PM_PEAK_LONE) || (tmpPeak->class & PM_PEAK_EDGE))) {
+                    printf("TEST ERROR: (0) peak at (%d, %d) (%f) ->class set improperly (0x%x).",
+                           tmpPeak->y, tmpPeak->x, tmpPeak->counts, tmpPeak->class);
+                    printf(" should be (0x%x or 0x%x).\n", PM_PEAK_LONE, PM_PEAK_EDGE);
+                    testStatus = false;
+                }
+            } else if ((tmpPeak->x == numCols/2) && (tmpPeak->y == numRows/2)) {
+                if (tmpPeak->class != PM_PEAK_LONE) {
+                    printf("TEST ERROR: (1) peak at (%d, %d) (%f) ->class set improperly (0x%x).\n",
+                           tmpPeak->y, tmpPeak->x, tmpPeak->counts, tmpPeak->class);
+                    printf(" should be (0x%x).\n", PM_PEAK_LONE);
+                    testStatus = false;
+                }
+            } else {
+                printf("TEST ERROR: Peak at (%d, %d) (%f)\n", tmpPeak->y, tmpPeak->x, tmpPeak->counts);
+                testStatus = false;
+            }
+        }
+    }
+
+    psFree(inData);
+    psFree(outData);
+    return(testStatus);
+}
+
+int test02( void )
+{
+    bool testStatus = true;
+    psArray *tmpArray = NULL;
+    psImage *tmpImageF64 = psImageAlloc(TST02_NUM_ROWS, TST02_NUM_COLS, PS_TYPE_F64);
+    psImage *tmpImageEmpty = psImageAlloc(0, 0, PS_TYPE_F32);
+
+    psTraceSetLevel(".", 0);
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmFindImagePeaks with NULL psImage.  Should generate error and return NULL.\n");
+    tmpArray = pmFindImagePeaks(NULL, 0.0);
+    if (tmpArray != NULL) {
+        printf("TEST ERROR: pmFindImagePeaks() returned a non-NULL psImage.\n");
+        testStatus = false;
+        psFree(tmpArray);
+    }
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmFindImagePeaks with empty psImage.  Should generate error and return NULL.\n");
+    tmpArray = pmFindImagePeaks(tmpImageEmpty, 0.0);
+    if (tmpArray != NULL) {
+        printf("TEST ERROR: pmFindImagePeaks() returned a non-NULL psImage.\n");
+        testStatus = false;
+        psFree(tmpArray);
+    }
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmFindImagePeaks with PS_TYPE_F64 psImage.  Should generate error and return NULL.\n");
+    tmpArray = pmFindImagePeaks(tmpImageF64, 0.0);
+    if (tmpArray != NULL) {
+        printf("TEST ERROR: pmFindImagePeaks() returned a non-NULL psImage.\n");
+        testStatus = false;
+        psFree(tmpArray);
+    }
+    printf("----------------------------------------------------------------------------------\n");
+    //    testStatus&= test_pmFindImagePeaks(1, 1);
+    //    testStatus&= test_pmFindImagePeaks(2, 5);
+    //    testStatus&= test_pmFindImagePeaks(5, 2);
+    // HEY: add code for small images
+    //    testStatus&= test_pmFindImagePeaks(1, 1);
+    //    testStatus&= test_pmFindImagePeaks(1, 8);
+    //    testStatus&= test_pmFindImagePeaks(8, 1);
+    testStatus&= test_pmFindImagePeaks(TST02_NUM_ROWS,   TST02_NUM_COLS);
+    testStatus&= test_pmFindImagePeaks(2*TST02_NUM_ROWS, TST02_NUM_COLS);
+    testStatus&= test_pmFindImagePeaks(TST02_NUM_ROWS,   2*TST02_NUM_COLS);
+
+
+    psFree(tmpImageF64);
+    psFree(tmpImageEmpty);
+    return(testStatus);
+}
+
+/******************************************************************************
+test03(): We first test pmCullPeaks() with various NULL and unallowable input
+parameters.  Then we generate a list of peaks and test that pmCullPeaks()
+removes them correctly.
+ *****************************************************************************/
+int test03( void )
+{
+    bool testStatus = true;
+    psImage *imgData = psImageAlloc(NUM_COLS, NUM_ROWS, PS_TYPE_F32);
+    psArray *outData = NULL;
+
+    /* XXX: Modify for new pmCullPeaks()
+            printf("----------------------------------------------------------------------------------\n");
+            printf("Calling pmCullPeaks with NULL psList.  Should generate error and return NULL.\n");
+            outData = pmCullPeaks(NULL, 0.0, NULL);
+            if (outData != NULL) {
+                printf("TEST ERROR: pmCulPeaks() returned a non-NULL psList.\n");
+                testStatus = false;
+        }
+    */
+
+    //
+    // Set peaks in input image.  All even-column and even-row pixels are
+    // set non-zero, all other pixels are set to zero.
+    //
+    psS32 numPeaksOrig = 0;
+    for (psS32 i = 0 ; i < NUM_ROWS ; i++) {
+        for (psS32 j = 0 ; j < NUM_COLS ; j++) {
+            if ((0 == i%2) && (0 == j%2)) {
+                imgData->data.F32[i][j] = (float) (i + 10);
+                numPeaksOrig++;
+            } else {
+                imgData->data.F32[i][j] = 0.0;
+            }
+        }
+    }
+    for (psS32 i = 0 ; i < NUM_ROWS ; i++) {
+        for (psS32 j = 0 ; j < NUM_COLS ; j++) {
+            printf("(%.1f) ", imgData->data.F32[i][j]);
+        }
+        printf("\n");
+    }
+    printf("Set %d peaks\n", numPeaksOrig);
+
+    //
+    // Call pmCullPeaks() with HUGE maxValue and NULL psRegion.  Should not
+    // remove any peaks.
+    //
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmCullPeaks with large maxValue and NULL psRegion.\n");
+    outData = pmFindImagePeaks(imgData, 0.0);
+    /* XXX: Modify for new pmCullPeaks
+        outData = pmCullPeaks(outData, PS_MAX_F32, NULL);
+
+        if (outData == NULL) {
+            printf("TEST ERROR: pmCullPeaks() returned a non-NULL psList.\n");
+            testStatus = false;
+            return(testStatus);
+        }
+        if (outData->n != numPeaksOrig) {
+            printf("TEST ERROR (0): pmCullPeaks incorrectly removed peaks\n");
+            printf("The pmCullPeaks() output had %d peaks, should have had %d peaks.\n", outData->n, numPeaksOrig);
+            testStatus = false;
+        }
+    */
+    psFree(outData);
+
+    //
+    // Call pmCullPeaks() with TINY maxValue and NULL psRegion.  Should
+    // remove all peaks.
+    //
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmCullPeaks with tiny maxValue and NULL psRegion.\n");
+    outData = pmFindImagePeaks(imgData, 0.0);
+    printf("pmFindImagePeaks found %ld peaks\n", outData->n);
+    /* XXX: Modify for new pmCullPeaks
+        outData = pmCullPeaks(outData, 0.0, NULL);
+
+        if (outData == NULL) {
+            printf("TEST ERROR: pmCullPeaks() returned a non-NULL psList.\n");
+            testStatus = false;
+            return(testStatus);
+        }
+        if (outData->n != 0) {
+            printf("TEST ERROR (1): pmCullPeaks incorrectly removed peaks\n");
+            printf("The pmCullPeaks() output had %d peaks, should have had %d peaks.\n", outData->n, 0);
+            testStatus = false;
+        }
+        psFree(outData);
+    */
+
+    //
+    // Call pmCullPeaks() with HUGE maxValue and disjoint psRegion.  Should
+    // not remove any peaks.
+    //
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmCullPeaks with large maxValue and disjoint psRegion.\n");
+    outData = pmFindImagePeaks(imgData, 0.0);
+    printf("pmFindImagePeaks found %ld peaks\n", outData->n);
+    psRegion tmpRegion = psRegionSet(10000.0, 20000.0, 10000.0, 20000.0);
+
+    /* XXX: Modify for new pmCullPeaks
+        outData = pmCullPeaks(outData, PS_MAX_F32, tmpRegion);
+
+        if (outData == NULL) {
+            printf("TEST ERROR: pmCullPeaks() returned a non-NULL psList.\n");
+            testStatus = false;
+            return(testStatus);
+        }
+        if (outData->n != numPeaksOrig) {
+            printf("TEST ERROR (2): pmCullPeaks incorrectly removed peaks\n");
+            printf("The pmCullPeaks() output had %d peaks, should have had %d peaks.\n", outData->n, numPeaksOrig);
+            testStatus = false;
+        }
+    */
+    psFree(outData);
+
+    //
+    // Call pmCullPeaks() with HUGE maxValue and non-disjoint psRegion.  Should
+    // remove all peaks.
+    //
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmCullPeaks with large maxValue and non-disjoint psRegion.\n");
+    outData = pmFindImagePeaks(imgData, 0.0);
+    printf("pmFindImagePeaks found %ld peaks\n", outData->n);
+    tmpRegion = psRegionSet(-PS_MAX_F32, PS_MAX_F32, -PS_MAX_F32, PS_MAX_F32);
+    /* XXX: Modify for new pmCullPeaks
+        outData = pmCullPeaks(outData, PS_MAX_F32, tmpRegion);
+
+        if (outData == NULL) {
+            printf("TEST ERROR: pmCullPeaks() returned a non-NULL psList.\n");
+            testStatus = false;
+            return(testStatus);
+        }
+        if (outData->n != 0) {
+            printf("TEST ERROR (3): pmCullPeaks incorrectly removed peaks\n");
+            printf("The pmCullPeaks() output had %d peaks, should have had %d peaks.\n", outData->n, 0);
+            testStatus = false;
+        }
+    */
+    psFree(outData);
+
+    printf("----------------------------------------------------------------------------------\n");
+    psFree(imgData);
+    return(testStatus);
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#define TST04_NUM_ROWS 100
+#define TST04_NUM_COLS 100
+#define TST04_SKY 20.0
+#define TST04_INNER_RADIUS 3
+#define TST04_OUTER_RADIUS 5
+/******************************************************************************
+test04(): We first test pmSourceLocalSky() with various NULL and unallowable
+input parameters.
+ 
+XXX: Should we produce tests with boundary numbers for the inner/outer radius?
+ 
+XXX: Call this with varying sizes for the image.
+ *****************************************************************************/
+int test04( void )
+{
+    bool testStatus = true;
+    psImage *imgData = psImageAlloc(TST04_NUM_COLS, TST04_NUM_ROWS, PS_TYPE_F32);
+    psImageInit(imgData, TST04_SKY);
+    psImage *imgMask = psImageAlloc(TST04_NUM_COLS, TST04_NUM_ROWS, PS_TYPE_U8);
+    psImageInit(imgMask, 0);
+    //    psImage *imgMaskS8 = psImageAlloc(TST04_NUM_COLS, TST04_NUM_ROWS, PS_TYPE_S8);
+    //    psImageInit(imgMaskS8, 0);
+    psImage *imgDataF64 = psImageAlloc(TST04_NUM_COLS, TST04_NUM_ROWS, PS_TYPE_F64);
+    psImageInit(imgDataF64, 0.0);
+    pmPeak *tmpPeak = pmPeakAlloc((psF32) (TST04_NUM_ROWS / 2),
+                                  (psF32) (TST04_NUM_COLS / 2),
+                                  200.0,
+                                  PM_PEAK_LONE);
+    pmSource *tmpSource = pmSourceAlloc();
+    tmpSource->pixels = imgData;
+    tmpSource->mask = imgMask;
+    tmpSource->peak = tmpPeak;
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceLocalSky with NULL tmpSource.  Should generate error and return FALSE.\n");
+    bool rc = pmSourceLocalSky(NULL, PS_STAT_SAMPLE_MEAN, 10.0);
+    if (rc != false) {
+        printf("TEST ERROR: pmSourceLocalSky() returned a non-FALSE pmSource.\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceLocalSky with Radius<0.0.  Should generate error and return FALSE.\n");
+    rc = pmSourceLocalSky(tmpSource, PS_STAT_SAMPLE_MEAN, -10.0);
+    if (rc != false) {
+        printf("TEST ERROR: pmSourceLocalSky() returned a non-FALSE pmSource.\n");
+        testStatus = false;
+    }
+
+    //
+    // XXX: The following code should be a separate function, and we should call it
+    // with a variety of image sizes, peaks.
+    //
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceLocalSky with valid data.\n");
+    tmpPeak->x = (psF32) (TST04_NUM_ROWS / 2);
+    tmpPeak->y = (psF32) (TST04_NUM_COLS / 2);
+    rc = pmSourceLocalSky(tmpSource, PS_STAT_SAMPLE_MEAN, 10.0);
+
+    if (rc == false) {
+        printf("TEST ERROR: pmSourceLocalSky() returned a FALSE pmSource.\n");
+        testStatus = false;
+    } else {
+        if (tmpSource->peak == NULL) {
+            printf("TEST ERROR: pmSourceLocalSky() returned a NULL pmSource->peak.\n");
+            testStatus = false;
+        } else {
+            if (tmpSource->peak->x != tmpPeak->x) {
+                printf("TEST ERROR: pmSourceLocalSky() pmSource->peak->x was %d, should have been %d.\n",
+                       tmpSource->peak->x, tmpPeak->x);
+                testStatus = false;
+            }
+
+            if (tmpSource->peak->y != tmpPeak->y) {
+                printf("TEST ERROR: pmSourceLocalSky() pmSource->peak->y was %d, should have been %d.\n",
+                       tmpSource->peak->y, tmpPeak->y);
+                testStatus = false;
+            }
+
+            if (tmpSource->peak->counts != tmpPeak->counts) {
+                printf("TEST ERROR: pmSourceLocalSky() pmSource->peak->counts was %f, should have been %f.\n",
+                       tmpSource->peak->counts, tmpPeak->counts);
+                testStatus = false;
+            }
+
+            if (tmpSource->peak->class != tmpPeak->class) {
+                printf("TEST ERROR: pmSourceLocalSky() pmSource->peak->class was %d, should have been %d.\n",
+                       tmpSource->peak->class, tmpPeak->class);
+                testStatus = false;
+            }
+        }
+
+        if (tmpSource->moments == NULL) {
+            printf("TEST ERROR: pmSourceLocalSky() returned a NULL pmSource->moments.\n");
+            testStatus = false;
+        } else {
+            if (tmpSource->moments->Sky != TST04_SKY) {
+                printf("TEST ERROR: pmSourceLocalSky() pmSource->moments->Sky was %f, should have been %f.\n", tmpSource->moments->Sky, TST04_SKY);
+                testStatus = false;
+            }
+        }
+    }
+
+    printf("----------------------------------------------------------------------------------\n");
+    psFree(tmpSource);
+    //    psFree(imgData);
+    //    psFree(imgDataF64);
+    //    psFree(imgMask);
+    //    psFree(imgMaskS8);
+    return(testStatus);
+}
+
+#define TST05_NUM_ROWS 100
+#define TST05_NUM_COLS 100
+#define TST05_SKY 20.0
+#define TST05_INNER_RADIUS 3
+#define TST05_OUTER_RADIUS 5
+/******************************************************************************
+test05(): We first test pmSourceMoments() with various NULL and unallowable
+input parameters.
+ 
+XXX: Should we produce tests with boundary numbers for the inner/outer radius?
+ 
+XXX: Call this with varying sizes for the image.
+ 
+XXX: The actual values of the moments are not tested.
+ *****************************************************************************/
+int test05( void )
+{
+    bool testStatus = true;
+    psImage *imgData = psImageAlloc(TST04_NUM_COLS, TST04_NUM_ROWS, PS_TYPE_F32);
+    psImageInit(imgData, TST04_SKY);
+    psImage *imgMask = psImageAlloc(TST04_NUM_COLS, TST04_NUM_ROWS, PS_TYPE_U8);
+    psImageInit(imgMask, 0);
+    pmPeak *tmpPeak = pmPeakAlloc((psF32) (TST04_NUM_ROWS / 2),
+                                  (psF32) (TST04_NUM_COLS / 2),
+                                  200.0,
+                                  PM_PEAK_LONE);
+    pmSource *tmpSource = pmSourceAlloc();
+    tmpSource->pixels = imgData;
+    tmpSource->mask = imgMask;
+    tmpSource->peak = tmpPeak;
+    psBool rc = pmSourceLocalSky(tmpSource, PS_STAT_SAMPLE_MEAN, 10.0);
+
+    if (rc == false) {
+        printf("TEST ERROR: pmSourceLocalSky() returned a FALSE pmSource.\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceMoments with NULL pmSource.  Should generate error and return FALSE.\n");
+    rc = pmSourceMoments(NULL, 10.0);
+    if (rc != false) {
+        printf("TEST ERROR: pmSourceMoments() returned TRUE.\n");
+        testStatus = false;
+    }
+    // XXX: test with pmSource->peaks NULL
+    // XXX: test with pmSource->pixels NULL
+    // XXX: test with pmSource->mask NULL
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceMoments with radius < 0.0.  Should generate error and return FALSE.\n");
+    rc = pmSourceMoments(tmpSource, -10.0);
+    if (rc != false) {
+        printf("TEST ERROR: pmSourceMoments() returned TRUE.\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------------\n");
+    psFree(tmpSource);
+    return(testStatus);
+
+}
+
+#define TST09_NUM_ROWS 70
+#define TST09_NUM_COLS 70
+#define TST09_SKY 5.0
+#define TST09_INNER_RADIUS 3
+#define TST09_OUTER_RADIUS 10
+#define LEVEL (TST09_SKY + 10.0)
+/******************************************************************************
+test09(): We first test pmSourceContour() with various NULL and unallowable
+input parameters.
+ 
+XXX: We don't verify the numbers.
+ *****************************************************************************/
+int test09( void )
+{
+    bool testStatus = true;
+    psImage *imgData = psImageAlloc(TST09_NUM_COLS, TST09_NUM_ROWS, PS_TYPE_F32);
+    psImageInit(imgData, TST09_SKY);
+    psImage *imgMask = psImageAlloc(TST09_NUM_COLS, TST09_NUM_ROWS, PS_TYPE_U8);
+    psImageInit(imgMask, 0);
+    pmPeak *tmpPeak = pmPeakAlloc((psF32) (TST09_NUM_ROWS / 2),
+                                  (psF32) (TST09_NUM_COLS / 2),
+                                  200.0,
+                                  PM_PEAK_LONE);
+    pmSource *tmpSource = pmSourceAlloc();
+    tmpSource->pixels = imgData;
+    tmpSource->mask = imgMask;
+    tmpSource->peak = tmpPeak;
+    psBool rc = pmSourceLocalSky(tmpSource, PS_STAT_SAMPLE_MEAN, 10.0);
+    if (rc == false) {
+        printf("TEST ERROR: pmSourceLocalSky() returned a FALSE pmSource.\n");
+        testStatus = false;
+    }
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceContour with NULL pmSource .  Should generate error, return FALSE.\n");
+    rc = pmSourceContour(NULL, imgData, LEVEL, PS_CONTOUR_CRUDE);
+    if (rc != false) {
+        printf("TEST ERROR: pmSourceContour() returned TRUE.\n");
+        testStatus = false;
+        psFree(rc);
+    }
+
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceContour with NULL psImage .  Should generate error, return FALSE.\n");
+    rc = pmSourceContour(tmpSource, NULL, LEVEL, PS_CONTOUR_CRUDE);
+    if (rc != FALSE) {
+        printf("TEST ERROR: pmSourceContour() returned TRUE.\n");
+        testStatus = false;
+        psFree(rc);
+    }
+
+    //
+    // XXX: pmSourceContour() has a problem with contour tops/bottoms.
+    // Must correct this.
+    //
+    if (1) {
+        printf("----------------------------------------------------------------------------------\n");
+        printf("Calling pmSourceContour with acceptable data.\n");
+        printf("NOTE: must figure out the parameters for this test to be meaningful.\n");
+        tmpSource->modelPSF->params->data.F32[0] = TST09_SKY;
+        tmpSource->modelPSF->params->data.F32[1] = 15.0;
+        tmpSource->modelPSF->params->data.F32[2] = (psF32) (TST09_NUM_ROWS / 2);
+        tmpSource->modelPSF->params->data.F32[3] = (psF32) (TST09_NUM_COLS / 2);
+        tmpSource->modelPSF->params->data.F32[4] = 2.0;
+        tmpSource->modelPSF->params->data.F32[5] = 2.0;
+        tmpSource->modelPSF->params->data.F32[6] = 2.0;
+        rc = pmSourceContour(tmpSource, imgData, LEVEL, PS_CONTOUR_CRUDE);
+        if (rc == false) {
+            printf("TEST ERROR: pmSourceContour() returned FALSE.\n");
+            testStatus = false;
+        } else {
+            psFree(rc);
+        }
+    }
+
+    psFree(tmpSource);
+    return(testStatus);
+}
+
+#define TST15_NUM_ROWS 100
+#define TST15_NUM_COLS 100
+#define TST15_SKY 10.0
+#define TST15_INNER_RADIUS 3
+#define TST15_OUTER_RADIUS 5
+/******************************************************************************
+test15(): We first test pmSourceAddModel() with various NULL and unallowable
+input parameters.
+ 
+XXX: We don't verify the numbers.
+ *****************************************************************************/
+/*
+int test15( void )
+{
+    bool testStatus = true;
+    psImage *imgData = psImageAlloc(TST15_NUM_COLS, TST15_NUM_ROWS, PS_TYPE_F32);
+    psImageInit(imgData, TST15_SKY);
+    psImage *imgMask = psImageAlloc(TST15_NUM_COLS, TST15_NUM_ROWS, PS_TYPE_U8);
+    psImageInit(imgMask, 0);
+    pmPeak *tmpPeak = pmPeakAlloc((psF32) (TST15_NUM_ROWS / 2),
+                                  (psF32) (TST15_NUM_COLS / 2),
+                                  200.0,
+                                  PM_PEAK_LONE);
+    pmSource *tmpSource = pmSourceAlloc();
+    tmpSource->pixels = imgData;
+    tmpSource->mask = imgMask;
+    tmpSource->peak = tmpPeak;
+    psBool rc = pmSourceLocalSky(tmpSource, PS_STAT_SAMPLE_MEAN, 10.0);
+    if (rc == false) {
+        printf("TEST ERROR: pmSourceLocalSky() returned a FALSE pmSource.\n");
+        testStatus = false;
+    }
+ 
+ 
+    tmpSource->modelPSF = pmModelAlloc(PS_MODEL_GAUSS);
+    tmpSource->modelPSF->params->data.F32[0] = 5.0;
+    tmpSource->modelPSF->params->data.F32[1] = 70.0;
+    tmpSource->modelPSF->params->data.F32[2] = (psF32) (TST15_NUM_ROWS / 2);
+    tmpSource->modelPSF->params->data.F32[3] = (psF32) (TST15_NUM_COLS / 2);
+    tmpSource->modelPSF->params->data.F32[4] = 1.0;
+    tmpSource->modelPSF->params->data.F32[5] = 1.0;
+    tmpSource->modelPSF->params->data.F32[6] = 2.0;
+ 
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceAddModel with NULL psImage.  Should generate error, return FALSE.\n");
+    rc = pmSourceAddModel(NULL, tmpSource, true);
+    if (rc == true) {
+        printf("TEST ERROR: pmSourceAddModel() returned TRUE.\n");
+        testStatus = false;
+    }
+ 
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceAddModel with NULL psSrc.  Should generate error, return FALSE.\n");
+    rc = pmSourceAddModel(imgData, NULL, true);
+    if (rc == true) {
+        printf("TEST ERROR: pmSourceAddModel() returned TRUE.\n");
+        testStatus = false;
+    }
+ 
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceAddModel with acceptable data.\n");
+    rc = pmSourceAddModel(imgData, tmpSource, true);
+    if (rc != true) {
+        printf("TEST ERROR: pmSourceAddModel() returned FALSE.\n");
+        testStatus = false;
+    }
+ 
+    psFree(tmpSource);
+    psFree(imgData);
+    return(testStatus);
+}
+*/
+
+#define TST16_NUM_ROWS 100
+#define TST16_NUM_COLS 100
+#define TST16_SKY 10.0
+#define TST16_INNER_RADIUS 3
+#define TST16_OUTER_RADIUS 5
+/******************************************************************************
+test16(): We first test pmSourceSubModel() with various NULL and unallowable
+input parameters.
+ 
+XXX: We don't verify the numbers.
+ *****************************************************************************/
+/*
+int test16( void )
+{
+    bool testStatus = true;
+    psImage *imgData = psImageAlloc(TST16_NUM_COLS, TST16_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < imgData->numRows; i++) {
+        for (psS32 j = 0 ; j < imgData->numCols; j++) {
+            imgData->data.F32[i][j] = TST16_SKY;
+        }
+    }
+    pmSource *tmpSource = NULL;
+    psBool rc = false;
+ 
+    pmPeak *tmpPeak = pmPeakAlloc((psF32) (TST16_NUM_ROWS / 2),
+                                  (psF32) (TST16_NUM_COLS / 2),
+                                  200.0,
+                                  PM_PEAK_LONE);
+ 
+    printf("Calling pmSourceLocalSky with valid data.\n");
+    tmpPeak->x = (psF32) (TST16_NUM_ROWS / 2);
+    tmpPeak->y = (psF32) (TST16_NUM_COLS / 2);
+    tmpSource = pmSourceLocalSky(imgData,
+                                 tmpPeak,
+                                 PS_STAT_SAMPLE_MEAN,
+                                 (psF32) TST16_INNER_RADIUS,
+                                 (psF32) TST16_OUTER_RADIUS);
+ 
+    if (tmpSource == NULL) {
+        printf("TEST ERROR: pmSourceLocalSky() returned a NULL pmSource.\n");
+        testStatus = false;
+    }
+ 
+    tmpSource->modelPSF = pmModelAlloc(PS_MODEL_GAUSS);
+    tmpSource->modelPSF->params->data.F32[0] = 5.0;
+    tmpSource->modelPSF->params->data.F32[1] = 70.0;
+    tmpSource->modelPSF->params->data.F32[2] = (psF32) (TST16_NUM_ROWS / 2);
+    tmpSource->modelPSF->params->data.F32[3] = (psF32) (TST16_NUM_COLS / 2);
+    tmpSource->modelPSF->params->data.F32[4] = 1.0;
+    tmpSource->modelPSF->params->data.F32[5] = 1.0;
+    tmpSource->modelPSF->params->data.F32[6] = 2.0;
+ 
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceSubModel with NULL psImage.  Should generate error, return FALSE.\n");
+    rc = pmSourceSubModel(NULL, tmpSource, true);
+    if (rc == true) {
+        printf("TEST ERROR: pmSourceSubModel() returned TRUE.\n");
+        testStatus = false;
+    }
+ 
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceSubModel with NULL psSrc.  Should generate error, return FALSE.\n");
+    rc = pmSourceSubModel(imgData, NULL, true);
+    if (rc == true) {
+        printf("TEST ERROR: pmSourceSubModel() returned TRUE.\n");
+        testStatus = false;
+    }
+ 
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceSubModel with acceptable data.\n");
+    rc = pmSourceSubModel(imgData, tmpSource, true);
+    if (rc != true) {
+        printf("TEST ERROR: pmSourceSubModel() returned FALSE.\n");
+        testStatus = false;
+    }
+ 
+    psFree(tmpSource);
+    psFree(imgData);
+    return(testStatus);
+}
+*/
+
+#define TST20_NUM_ROWS 100
+#define TST20_NUM_COLS 100
+#define TST20_SKY 10.0
+#define TST20_INNER_RADIUS 3
+#define TST20_OUTER_RADIUS 5
+/******************************************************************************
+test20(): We first test pmSourceSubModel() with various NULL and unallowable
+input parameters.
+ 
+XXX: We don't verify the numbers.
+ *****************************************************************************/
+/*
+int test20( void )
+{
+    bool testStatus = true;
+    psImage *imgData = psImageAlloc(TST20_NUM_COLS, TST20_NUM_ROWS, PS_TYPE_F32);
+    for (psS32 i = 0 ; i < imgData->numRows; i++) {
+        for (psS32 j = 0 ; j < imgData->numCols; j++) {
+            imgData->data.F32[i][j] = TST20_SKY;
+        }
+    }
+    pmSource *tmpSource = NULL;
+    psBool rc = false;
+ 
+    pmPeak *tmpPeak = pmPeakAlloc((psF32) (TST20_NUM_ROWS / 2),
+                                  (psF32) (TST20_NUM_COLS / 2),
+                                  200.0,
+                                  PM_PEAK_LONE);
+ 
+    printf("Calling pmSourceLocalSky with valid data.\n");
+    tmpPeak->x = (psF32) (TST20_NUM_ROWS / 2);
+    tmpPeak->y = (psF32) (TST20_NUM_COLS / 2);
+    tmpSource = pmSourceLocalSky(imgData,
+                                 tmpPeak,
+                                 PS_STAT_SAMPLE_MEAN,
+                                 (psF32) TST20_INNER_RADIUS,
+                                 (psF32) TST20_OUTER_RADIUS);
+ 
+    if (tmpSource == NULL) {
+        printf("TEST ERROR: pmSourceLocalSky() returned a NULL pmSource.\n");
+        testStatus = false;
+    }
+ 
+    tmpSource->modelPSF = pmModelAlloc(PS_MODEL_GAUSS);
+ 
+ 
+    tmpSource->modelPSF->params->data.F32[0] = 5.0;
+    tmpSource->modelPSF->params->data.F32[1] = 70.0;
+    tmpSource->modelPSF->params->data.F32[2] = (psF32) (TST20_NUM_ROWS / 2);
+    tmpSource->modelPSF->params->data.F32[3] = (psF32) (TST20_NUM_COLS / 2);
+    tmpSource->modelPSF->params->data.F32[4] = 1.0;
+    tmpSource->modelPSF->params->data.F32[5] = 1.0;
+    tmpSource->modelPSF->params->data.F32[6] = 2.0;
+ 
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceFitModel with NULL psImage.  Should generate error, return FALSE.\n");
+    rc = pmSourceFitModel(tmpSource, NULL);
+    if (rc == true) {
+        printf("TEST ERROR: pmSourceFitModel() returned TRUE.\n");
+        testStatus = false;
+    }
+ 
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceFitModel with NULL pmSource.  Should generate error, return FALSE.\n");
+    rc = pmSourceFitModel(NULL, imgData);
+    if (rc == true) {
+        printf("TEST ERROR: pmSourceFitModel() returned TRUE.\n");
+        testStatus = false;
+    }
+ 
+    printf("----------------------------------------------------------------------------------\n");
+    printf("Calling pmSourceFitModel with acceptable data.\n");
+    rc = pmSourceFitModel(tmpSource, imgData);
+    printf("pmSourceFitModel returned %d\n", rc);
+ 
+    // XXX: Memory leaks are not being tested
+    psVector *junk = psVectorAlloc(10, PS_TYPE_F32);
+    junk->data.F32[0] = 0.0;
+ 
+    psFree(tmpSource);
+    psFree(imgData);
+    return(testStatus);
+}
+*/
+
Index: /branches/eam_branch_20071023/psModules/test/pstap/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/pstap/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/pstap/.cvsignore	(revision 15358)
@@ -0,0 +1,2 @@
+Makefile
+Makefile.in
Index: /branches/eam_branch_20071023/psModules/test/pstap/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/pstap/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/pstap/Makefile.am	(revision 15358)
@@ -0,0 +1,1 @@
+SUBDIRS = src
Index: /branches/eam_branch_20071023/psModules/test/pstap/src/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/pstap/src/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/pstap/src/.cvsignore	(revision 15358)
@@ -0,0 +1,6 @@
+Makefile
+Makefile.in
+libpstap.la
+pstap.lo
+.deps
+.libs
Index: /branches/eam_branch_20071023/psModules/test/pstap/src/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/pstap/src/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/pstap/src/Makefile.am	(revision 15358)
@@ -0,0 +1,18 @@
+
+AM_CPPFLAGS = \
+	$(SRCINC) \
+	-I$(top_srcdir)/test/tap/src \
+	-I$(top_srcdir)/test/pstap/src \
+	$(PSMODULES_CFLAGS)
+
+AM_LDFLAGS = \
+	$(top_builddir)/src/libpsmodules.la  \
+	$(top_builddir)/test/tap/src/libtap.la \
+	$(PSMODULES_LIBS)
+
+TEST_LTLIBS = libpstap.la
+libpstap_la_SOURCES = pstap.c
+noinst_HEADERS = pstap.h
+
+noinst_LTLIBRARIES = $(TEST_LTLIBS)
+
Index: /branches/eam_branch_20071023/psModules/test/pstap/src/pstap.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/pstap/src/pstap.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/pstap/src/pstap.c	(revision 15358)
@@ -0,0 +1,6 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <tap.h>
+#include <pslib.h>
Index: /branches/eam_branch_20071023/psModules/test/pstap/src/pstap.h
===================================================================
--- /branches/eam_branch_20071023/psModules/test/pstap/src/pstap.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/pstap/src/pstap.h	(revision 15358)
@@ -0,0 +1,63 @@
+#include <pslib.h>
+
+#include "tap.h"
+
+#define done() ok(psMemCheckLeaks(0, NULL, stdout, false) == 0, "Memory Leaks"); return exit_status()
+
+        # define mem() ok(psMemCheckLeaks(psMemGetLastId(), NULL, stdout, false) == 0, "Memory Leaks")
+
+        # define checkLeaks false
+
+        # define checkMem() if(checkLeaks) mem()
+
+            # ifdef __GNUC__
+
+            // write a comment which is counted as a test (and swallowed by prove)
+            # define note(A, ...) _gen_result(1, __func__, __FILE__, __LINE__, A, ## __VA_ARGS__);
+
+// use to test the value of a float
+# define ok_float(VALUE,EXPECT,COMMENT, ...)\
+ok((fabsf((VALUE)-(EXPECT)) < FLT_EPSILON), COMMENT, ## __VA_ARGS__);
+
+// use to test the value of a double
+# define ok_double(VALUE,EXPECT,COMMENT, ...)\
+ok((fabs((VALUE)-(EXPECT)) < DBL_EPSILON), COMMENT, ## __VA_ARGS__);
+
+// use to test the value of a float within a defined tolerance
+# define ok_float_tol(VALUE,EXPECT,TOL,COMMENT, ...)\
+ok((fabsf((VALUE)-(EXPECT)) < (TOL)), COMMENT, ## __VA_ARGS__);
+
+// use to test the value of a double within a defined tolerance
+# define ok_double_tol(VALUE,EXPECT,TOL,COMMENT, ...)\
+ok((fabs((VALUE)-(EXPECT)) < (TOL)), COMMENT, ## __VA_ARGS__);
+
+# define ok_str(VALUE,EXPECT,COMMENT, ...)\
+ok(strcmp(VALUE, EXPECT) == 0, COMMENT, ## __VA_ARGS__);
+
+#elif __STDC_VERSION__ >= 199901L /* __GNUC__ */
+
+// write a comment which is counted as a test (and swallowed by prove)
+# define note(A, ...) _gen_result(1, __func__, __FILE__, __LINE__, A, ...);
+
+// use to test the value of a float
+# define ok_float(VALUE,EXPECT, ...)\
+ok((fabsf((VALUE)-(EXPECT)) < FLT_EPSILON), __VA_ARGS__);
+
+// use to test the value of a double
+# define ok_double(VALUE,EXPECT, ...)\
+ok((fabs((VALUE)-(EXPECT)) < DBL_EPSILON), __VA_ARGS__);
+
+// use to test the value of a float
+# define ok_float_tol(VALUE,EXPECT,TOL, ...)\
+ok((fabsf((VALUE)-(EXPECT)) < (TOL)), __VA_ARGS__);
+
+// use to test the value of a double
+# define ok_double_tol(VALUE,EXPECT,TOL, ...)\
+ok((fabs((VALUE)-(EXPECT)) < )(TOL)), __VA_ARGS__);
+
+# define ok_str(VALUE,EXPECT, ...)\
+ok(strcmp(VALUE, EXPECT) == 0, __VA_ARGS__);
+
+#else /* __STDC_VERSION__ */
+# error "Needs gcc or C99 compiler for variadic macros."
+#endif /* __STDC_VERSION__ */
Index: /branches/eam_branch_20071023/psModules/test/tap/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/.cvsignore	(revision 15358)
@@ -0,0 +1,13 @@
+.in
+Makefile
+Makefile.in
+aclocal.m4
+autom4te.cache
+config.log
+config.status
+configure
+libtool
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/INSTALL
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/INSTALL	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/INSTALL	(revision 15358)
@@ -0,0 +1,8 @@
+Quick Installation
+
+    ./configure
+    make
+    make check
+    make install
+
+Run "configure --help" for additional options.
Index: /branches/eam_branch_20071023/psModules/test/tap/LICENSE
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/LICENSE	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/LICENSE	(revision 15358)
@@ -0,0 +1,23 @@
+Copyright (c) 2004 Nik Clayton
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
Index: /branches/eam_branch_20071023/psModules/test/tap/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/Makefile.am	(revision 15358)
@@ -0,0 +1,5 @@
+SUBDIRS  = src
+#SUBDIRS += tests
+
+prove:
+	prove -v -r
Index: /branches/eam_branch_20071023/psModules/test/tap/README
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/README	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/README	(revision 15358)
@@ -0,0 +1,11 @@
+NAME
+     tap -- write tests that implement the Test Anything Protocol
+
+SYNOPSIS
+     #include <tap.h>
+
+DESCRIPTION
+     The tap library provides functions for writing test scripts that produce
+     output consistent with the Test Anything Protocol.  A test harness that
+     parses this protocol can run these tests and produce useful reports indi-
+     cating their success or failure.
Index: /branches/eam_branch_20071023/psModules/test/tap/bootstrap.sh
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/bootstrap.sh	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/bootstrap.sh	(revision 15358)
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+set -x
+aclocal19 -I /usr/local/share/aclocal || aclocal || exit 1
+autoheader259 || autoheader || exit 1
+libtoolize15 -c -f || libtoolize -c -f || glibtoolize -c -f || exit 1
+automake19 -a -c || automake -a -c || exit 1
+autoconf259 || autoconf || exit 1
Index: /branches/eam_branch_20071023/psModules/test/tap/compile
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/compile	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/compile	(revision 15358)
@@ -0,0 +1,142 @@
+#! /bin/sh
+# Wrapper for compilers which do not understand `-c -o'.
+
+scriptversion=2004-10-12.08
+
+# Copyright (C) 1999, 2000, 2003, 2004 Free Software Foundation, Inc.
+# Written by Tom Tromey <tromey@cygnus.com>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# This file is maintained in Automake, please report
+# bugs to <bug-automake@gnu.org> or send patches to
+# <automake-patches@gnu.org>.
+
+case $1 in
+  '')
+     echo "$0: No command.  Try \`$0 --help' for more information." 1>&2
+     exit 1;
+     ;;
+  -h | --h*)
+    cat <<\EOF
+Usage: compile [--help] [--version] PROGRAM [ARGS]
+
+Wrapper for compilers which do not understand `-c -o'.
+Remove `-o dest.o' from ARGS, run PROGRAM with the remaining
+arguments, and rename the output as expected.
+
+If you are trying to build a whole package this is not the
+right script to run: please start by reading the file `INSTALL'.
+
+Report bugs to <bug-automake@gnu.org>.
+EOF
+    exit 0
+    ;;
+  -v | --v*)
+    echo "compile $scriptversion"
+    exit 0
+    ;;
+esac
+
+ofile=
+cfile=
+eat=
+
+for arg
+do
+  if test -n "$eat"; then
+    eat=
+  else
+    case $1 in
+      -o)
+	# configure might choose to run compile as `compile cc -o foo foo.c'.
+	# So we strip `-o arg' only if arg is an object.
+	eat=1
+	case $2 in
+	  *.o | *.obj)
+	    ofile=$2
+	    ;;
+	  *)
+	    set x "$@" -o "$2"
+	    shift
+	    ;;
+	esac
+	;;
+      *.c)
+	cfile=$1
+	set x "$@" "$1"
+	shift
+	;;
+      *)
+	set x "$@" "$1"
+	shift
+	;;
+    esac
+  fi
+  shift
+done
+
+if test -z "$ofile" || test -z "$cfile"; then
+  # If no `-o' option was seen then we might have been invoked from a
+  # pattern rule where we don't need one.  That is ok -- this is a
+  # normal compilation that the losing compiler can handle.  If no
+  # `.c' file was seen then we are probably linking.  That is also
+  # ok.
+  exec "$@"
+fi
+
+# Name of file we expect compiler to create.
+cofile=`echo "$cfile" | sed -e 's|^.*/||' -e 's/\.c$/.o/'`
+
+# Create the lock directory.
+# Note: use `[/.-]' here to ensure that we don't use the same name
+# that we are using for the .o file.  Also, base the name on the expected
+# object file name, since that is what matters with a parallel build.
+lockdir=`echo "$cofile" | sed -e 's|[/.-]|_|g'`.d
+while true; do
+  if mkdir "$lockdir" >/dev/null 2>&1; then
+    break
+  fi
+  sleep 1
+done
+# FIXME: race condition here if user kills between mkdir and trap.
+trap "rmdir '$lockdir'; exit 1" 1 2 15
+
+# Run the compile.
+"$@"
+ret=$?
+
+if test -f "$cofile"; then
+  mv "$cofile" "$ofile"
+elif test -f "${cofile}bj"; then
+  mv "${cofile}bj" "$ofile"
+fi
+
+rmdir "$lockdir"
+exit $ret
+
+# Local Variables:
+# mode: shell-script
+# sh-indentation: 2
+# eval: (add-hook 'write-file-hooks 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-end: "$"
+# End:
Index: /branches/eam_branch_20071023/psModules/test/tap/configure.in
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/configure.in	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/configure.in	(revision 15358)
@@ -0,0 +1,44 @@
+AC_INIT(tap, 1.01)
+AC_CONFIG_SRCDIR(src/tap.c)
+AM_INIT_AUTOMAKE([foreign])
+AC_CONFIG_HEADERS([src/config.h])
+AC_GNU_SOURCE
+AC_PROG_CC
+AC_PROG_LIBTOOL
+AC_PROG_INSTALL
+
+# Checks for libraries
+case "$host" in
+	*-*-*freebsd4*)
+		LDFLAGS="$LDFLAGS -pthread"
+		HAVE_LIBPTHREAD=1
+		;;
+	*)
+		AC_CHECK_LIB(pthread, main)
+		;;
+esac
+
+dnl build tests at the same time as the source code
+AC_ARG_ENABLE(tests,
+  [AS_HELP_STRING(--enable-tests,build tests at same time as source)],
+  [AC_MSG_RESULT(test building enabled)
+   tests=true],
+   [tests=false])
+AM_CONDITIONAL(BUILD_TESTS, test x$tests = xtrue)
+
+# Checks for header files
+AC_HEADER_STDC
+AC_CHECK_HEADERS([stdlib.h])
+AC_CHECK_HEADERS([pthread.h])
+
+# Checks for  typedefs, structures, and compiler characteristics.
+AC_C_CONST
+
+# Checks for library functions.
+AC_FUNC_VPRINTF
+AC_CHECK_FUNCS([atexit])
+
+AC_CONFIG_FILES([Makefile
+		 src/Makefile
+		])
+AC_OUTPUT
Index: /branches/eam_branch_20071023/psModules/test/tap/src/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/src/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/src/.cvsignore	(revision 15358)
@@ -0,0 +1,13 @@
+.deps
+.libs
+Makefile
+Makefile.in
+libtap.la
+tap.lo
+config.h
+config.h.in
+stamp-h1
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/src/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/src/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/src/Makefile.am	(revision 15358)
@@ -0,0 +1,8 @@
+TEST_LTLIBS = libtap.la
+libtap_la_SOURCES = tap.c tap.h
+noinst_HEADERS = tap.h
+noinst_LTLIBRARIES = $(TEST_LTLIBS)
+
+#man_MANS = tap.3
+EXTRA_DIST = $(man_MANS)
+
Index: /branches/eam_branch_20071023/psModules/test/tap/src/tap.3
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/src/tap.3	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/src/tap.3	(revision 15358)
@@ -0,0 +1,380 @@
+.Dd December 20, 2004
+.Os
+.Dt TAP 3
+.Sh NAME
+.Nm tap
+.Nd write tests that implement the Test Anything Protocol
+.Sh SYNOPSIS
+.In tap.h
+.Sh DESCRIPTION
+The
+.Nm
+library provides functions for writing test scripts that produce output
+consistent with the Test Anything Protocol.  A test harness that parses
+this protocol can run these tests and produce useful reports indicating
+their success or failure.
+.Ss PRINTF STRINGS
+In the descriptions that follow, for any function that takes as the
+last two parameters
+.Dq Fa char * , Fa ...
+it can be assumed that the
+.Fa char *
+is a
+.Fn printf
+-like format string, and the optional arguments are values to be placed
+in that string.
+.Ss TEST PLANS
+.Bl -tag -width indent
+.It Xo
+.Ft int
+.Fn plan_tests "unsigned int"
+.Xc
+.It Xo
+.Ft int
+.Fn plan_no_plan "void"
+.Xc
+.It Xo
+.Ft int
+.Fn plan_skip_all "char *" "..."
+.Xc
+.El
+.Pp
+You must first specify a test plan.  This indicates how many tests you
+intend to run, and allows the test harness to notice if any tests were
+missed, or if the test program exited prematurely.
+.Pp
+To do this, use
+.Fn plan_tests ,
+which returns the number of planned tests.  The function will cause
+your program to exit prematurely if you specify 0 tests.
+.Pp
+In some situations you may not know how many tests you will be running, or
+you are developing your test program, and do not want to update the
+.Fn plan_tests
+parameter every time you make a change.  For those situations use
+.Fn plan_no_plan .
+It returns 1, and indicates to the test harness that an indeterminate number
+of tests will be run.
+.Pp
+Both
+.Fn plan_tests
+and
+.Fn plan_no_plan
+will cause your test program to exit prematurely with a diagnostic
+message if they are called more than once.
+.Pp
+If your test program detects at run time that some required functionality
+is missing (for example, it relies on a database connection which is not
+present, or a particular configuration option that has not been included
+in the running kernel) use
+.Fn plan_skip_all ,
+passing as parameters a string to display indicating the reason for skipping
+the tests.
+.Ss SIMPLE TESTS
+.Bl -tag -width indent
+.It Xo
+.Ft unsigned int
+.Fn ok "expression" "char *" "..."
+.Xc
+.It Xo
+.Ft unsigned int
+.Fn ok1 "expression"
+.Xc
+.It Xo
+.Ft unsigned int
+.Fn pass "char *" "..."
+.Xc
+.It Xo
+.Ft unsigned int
+.Fn fail "char *" "..."
+.Xc
+.El
+.Pp
+Tests are implemented as expressions checked by calls to the
+.Fn ok
+and
+.Fn ok1
+macros.  In both cases
+.Fa expression
+should evaluate to true if the test succeeded.
+.Pp
+.Fn ok
+allows you to specify a name, or comment, describing the test which will
+be included in the output.
+.Fn ok1
+is for those times when the expression to be tested is self
+explanatory and does not need an associated comment.  In those cases
+the test expression becomes the comment.
+.Pp
+These four calls are equivalent:
+.Bd -literal -offset indent
+int i = 5;
+
+ok(i == 5, "i equals 5");      /* Overly verbose */
+ok(i == 5, "i equals %d", i);  /* Just to demonstrate printf-like
+                                  behaviour of the test name */
+ok(i == 5, "i == 5");          /* Needless repetition */
+ok1(i == 5);                   /* Just right */
+.Ed
+.Pp
+It is good practice to ensure that the test name describes the meaning
+behind the test rather than what you are testing.  Viz
+.Bd -literal -offset indent
+ok(db != NULL, "db is not NULL");            /* Not bad, but */
+ok(db != NULL, "Database conn. succeeded");  /* this is better */
+.Ed
+.Pp
+.Fn ok
+and
+.Fn ok1
+return 1 if the expression evaluated to true, and 0 if it evaluated to
+false.  This lets you chain calls from
+.Fn ok
+to
+.Fn diag
+to only produce diagnostic output if the test failed.  For example, this
+code will include diagnostic information about why the database connection
+failed, but only if the test failed.
+.Bd -literal -offset indent
+ok(db != NULL, "Database conn. succeeded") ||
+    diag("Database error code: %d", dberrno);
+.Ed
+.Pp
+You also have
+.Fn pass
+and
+.Fn fail .
+From the Test::More documentation:
+.Bd -literal -offset indent
+Sometimes you just want to say that the tests have passed.
+Usually the case is you've got some complicated condition
+that is difficult to wedge into an ok().  In this case,
+you can simply use pass() (to declare the test ok) or fail
+(for not ok).
+
+Use these very, very, very sparingly.
+.Ed
+.Pp
+These are synonyms for ok(1, ...) and ok(0, ...).
+.Ss SKIPPING TESTS
+.Bl -tag -width indent
+.It Xo
+.Ft int
+.Fn skip "unsigned int" "char *" "..."
+.Xc
+.It Xo
+.Fn skip_start "expression" "unsigned int" "char *" "..."
+.Xc
+.It Xo
+.Fn skip_end
+.Xc
+.El
+.Pp
+Sets of tests can be skipped.  Ordinarily you would do this because
+the test can't be run in this particular testing environment.
+.Pp
+For example, suppose some tests should be run as root.  If the test is
+not being run as root then the tests should be skipped.  In this 
+implementation, skipped tests are flagged as being ok, with a special
+message indicating that they were skipped.  It is your responsibility
+to ensure that the number of tests skipped (the first parameter to
+.Fn skip )
+is correct for the number of tests to skip.
+.Pp
+One way of implementing this is with a
+.Dq do { } while(0);
+loop, or an
+.Dq if( ) { } else { }
+construct, to ensure that there are no additional side effects from the
+skipped tests.
+.Bd -literal -offset indent
+if(getuid() != 0) {
+        skip(1, "because test only works as root");
+} else {
+        ok(do_something_as_root() == 0, "Did something as root");
+}
+.Ed
+.Pp
+Two macros are provided to assist with this.  The previous example could
+be re-written as follows.
+.Bd -literal -offset indent
+skip_start(getuid() != 0, 1, "because test only works as root");
+
+ok(do_something_as_root() == 0, "Did something as root");
+
+skip_end();
+.Ed
+.Ss MARKING TESTS AS Dq TODO
+.Bl -tag -width indent
+.It Xo
+.Ft void
+.Fn todo_start "char *" "..."
+.Xc
+.It Xo
+.Ft void
+.Fn todo_end "void"
+.Xc
+.El
+.Pp
+Sets of tests can be flagged as being
+.Dq TODO .
+These are tests that you expect to fail, probably because you haven't
+fixed a bug, or finished a new feature yet.  These tests will still be
+run, but with additional output that indicates that they are expected
+to fail.  Should a test start to succeed unexpectedly, tools like
+.Xr prove 1
+will indicate this, and you can move the test out of the todo
+block.  This is much more useful than simply commenting out (or
+.Dq #ifdef 0 ... #endif )
+the tests.
+.Bd -literal -offset indent
+todo_start("dwim() not returning true yet");
+
+ok(dwim(), "Did what the user wanted");
+
+todo_end();
+.Ed
+.Pp
+Should
+.Fn dwim
+ever start succeeding you will know about it as soon as you run the
+tests.  Note that
+.Em unlike
+the
+.Fn skip_*
+family, additional code between
+.Fn todo_start
+and
+.Fn todo_end
+.Em is
+executed.
+.Ss SKIP vs. TODO
+From the Test::More documentation;
+.Bd -literal -offset indent
+If it's something the user might not be able to do, use SKIP.
+This includes optional modules that aren't installed, running
+under an OS that doesn't have some feature (like fork() or
+symlinks), or maybe you need an Internet connection and one
+isn't available.
+
+If it's something the programmer hasn't done yet, use TODO.
+This is for any code you haven't written yet, or bugs you have
+yet to fix, but want to put tests in your testing script 
+(always a good idea).
+.Ed
+.Ss DIAGNOSTIC OUTPUT
+.Bl -tag -width indent
+.It Xo
+.Fr unsigned int
+.Fn diag "char *" "..."
+.Xc
+.El
+.Pp
+If your tests need to produce diagnostic output, use
+.Fn diag .
+It ensures that the output will not be considered by the TAP test harness.
+.Fn diag
+adds the necessary trailing
+.Dq \en
+for you.
+.Bd -literal -offset indent
+diag("Expected return code 0, got return code %d", rcode);
+.Ed
+.Pp
+.Fn diag
+always returns 0.
+.Ss EXIT STATUS
+.Bl -tag -width indent
+.It Xo
+.Fr int
+.Fn exit_status void
+.Xc
+.El
+.Pp
+For maximum compatability your test program should return a particular
+exit code.  This is calculated by
+.Fn exit_status
+so it is sufficient to always return from
+.Fn main
+with either
+.Dq return exit_status();
+or
+.Dq exit(exit_status());
+as appropriate.
+.Sh ENVIRONMENT
+The following environment variables affect
+.Nm .
+.Bl -tag -width indent
+.It Ev HARNESS_ACTIVE
+Causes an extra
+.Dq \en
+to be printed before any diagnostic failure output generated by
+.Nm .
+This variable is normally set if tests are being run under Perl's
+Test::Harness.
+.El
+.Sh EXAMPLES
+The
+.Pa tests
+directory in the source distribution contains numerous tests of
+.Nm
+functionality, written using
+.Nm .
+Examine them for examples of how to construct test suites.
+.Sh COMPATABILITY
+.Nm
+strives to be compatible with the Perl Test::More and Test::Harness 
+modules.  The test suite verifies that
+.Nm
+is bug-for-bug compatible with their behaviour.  This is why some
+functions which would more naturally return nothing return constant
+values.
+.Pp
+If the
+.Lb libpthread
+is found at compile time,
+.Nm
+.Em should
+be thread safe.  Indications to the contrary (and test cases that expose
+incorrect behaviour) are very welcome.
+.Sh SEE ALSO
+.Xr Test::More 1 ,
+.Xr Test::Harness 1 ,
+.Xr prove 1
+.Sh STANDARDS
+.Nm
+requires a
+.St -isoC-99
+compiler.  Some of the
+.Nm
+functionality is implemented as variadic macros, and that functionality
+was not formally codified until C99.  Patches to use
+.Nm
+with earlier compilers that have their own implementation of variadic
+macros will be gratefully received.
+.Sh HISTORY
+.Nm
+was written to help improve the quality and coverage of the FreeBSD
+regression test suite, and released in the hope that others find it
+a useful tool to help improve the quality of their code.
+.Sh AUTHORS
+.An "Nik Clayton" Aq nik@ngo.org.uk ,
+.Aq nik@FreeBSD.org
+.Pp
+.Nm
+would not exist without the efforts of
+.An "Michael G Schwern" Aq schqern@pobox.com ,
+.An "Andy Lester" Aq andy@petdance.com ,
+and the countless others who have worked on the Perl QA programme.
+.Sh BUGS
+Ideally, running the tests would have no side effects on the behaviour
+of the application you are testing.  However, it is not always possible
+to avoid them.  The following side effects of using
+.Nm
+are known.
+.Bl -bullet -offset indent
+.It
+stdout is set to unbuffered mode after calling any of the
+.Fn plan_*
+functions.
+.El
Index: /branches/eam_branch_20071023/psModules/test/tap/src/tap.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/src/tap.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/src/tap.c	(revision 15358)
@@ -0,0 +1,430 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <ctype.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "tap.h"
+
+static int no_plan = 0;
+static int skip_all = 0;
+static int have_plan = 0;
+static unsigned int test_count = 0; /* Number of tests that have been run */
+static unsigned int e_tests = 0; /* Expected number of tests to run */
+static unsigned int failures = 0; /* Number of tests that failed */
+static char *todo_msg = NULL;
+static char *todo_msg_fixed = "libtap malloc issue";
+static int todo = 0;
+static int test_died = 0;
+
+/* Encapsulate the pthread code in a conditional.  In the absence of
+   libpthread the code does nothing */
+#ifdef HAVE_LIBPTHREAD
+#include <pthread.h>
+static pthread_mutex_t M = PTHREAD_MUTEX_INITIALIZER;
+# define LOCK pthread_mutex_lock(&M);
+# define UNLOCK pthread_mutex_unlock(&M);
+#else
+# define LOCK
+# define UNLOCK
+#endif
+
+static void _expected_tests(unsigned int);
+static void _tap_init(void);
+static void _cleanup(void);
+
+/*
+ * Generate a test result.
+ *
+ * ok -- boolean, indicates whether or not the test passed.
+ * test_name -- the name of the test, may be NULL
+ * test_comment -- a comment to print afterwards, may be NULL
+ */
+unsigned int
+_gen_result(int ok, const char *func, char *file, unsigned int line,
+            char *test_name, ...)
+{
+    va_list ap;
+    char *local_test_name = NULL;
+    char *c;
+    int name_is_digits;
+
+    LOCK;
+
+    test_count++;
+
+    /* Start by taking the test name and performing any printf()
+       expansions on it */
+    if(test_name != NULL) {
+        va_start(ap, test_name);
+        vasprintf(&local_test_name, test_name, ap);
+        va_end(ap);
+
+        /* Make sure the test name contains more than digits
+           and spaces.  Emit an error message and exit if it
+           does */
+        if(local_test_name) {
+            name_is_digits = 1;
+            for(c = local_test_name; *c != '\0'; c++) {
+                if(!isdigit(*c) && !isspace(*c)) {
+                    name_is_digits = 0;
+                    break;
+                }
+            }
+
+            if(name_is_digits) {
+                diag("    You named your test '%s'.  You shouldn't use numbers for your test names.", local_test_name);
+                diag("    Very confusing.");
+            }
+        }
+    }
+
+    if(!ok) {
+        printf("not ");
+        failures++;
+    }
+
+    printf("ok %d", test_count);
+
+    if(test_name != NULL) {
+        printf(" - ");
+
+        /* Print the test name, escaping any '#' characters it
+           might contain */
+        if(local_test_name != NULL) {
+            flockfile(stdout);
+            for(c = local_test_name; *c != '\0'; c++) {
+                if(*c == '#')
+                    fputc('\\', stdout);
+                fputc((int)*c, stdout);
+            }
+            funlockfile(stdout);
+        } else { /* vasprintf() failed, use a fixed message */
+            printf("%s", todo_msg_fixed);
+        }
+    }
+
+    /* If we're in a todo_start() block then flag the test as being
+       TODO.  todo_msg should contain the message to print at this
+       point.  If it's NULL then asprintf() failed, and we should
+       use the fixed message.
+
+       This is not counted as a failure, so decrement the counter if
+       the test failed. */
+    if(todo) {
+        printf(" # TODO %s", todo_msg ? todo_msg : todo_msg_fixed);
+        if(!ok)
+            failures--;
+    }
+
+    printf("\n");
+
+    if(!ok) {
+        if(getenv("HARNESS_ACTIVE") != NULL)
+            fputs("\n", stderr);
+
+        diag("    Failed %stest (%s:%s() at line %d)",
+             todo ? "(TODO) " : "", file, func, line);
+    }
+    free(local_test_name);
+
+    UNLOCK;
+
+    /* We only care (when testing) that ok is positive, but here we
+       specifically only want to return 1 or 0 */
+    return ok ? 1 : 0;
+}
+
+/*
+ * Initialise the TAP library.  Will only do so once, however many times it's
+ * called.
+ */
+void
+_tap_init(void)
+{
+    static int run_once = 0;
+
+    if(!run_once) {
+        atexit(_cleanup);
+
+        /* stdout needs to be unbuffered so that the output appears
+           in the same place relative to stderr output as it does 
+           with Test::Harness */
+        setbuf(stdout, 0);
+        run_once = 1;
+    }
+}
+
+/*
+ * Note that there's no plan.
+ */
+int
+plan_no_plan(void)
+{
+
+    LOCK;
+
+    _tap_init();
+
+    if(have_plan != 0) {
+        fprintf(stderr, "You tried to plan twice!\n");
+        test_died = 1;
+        UNLOCK;
+        exit(255);
+    }
+
+    have_plan = 1;
+    no_plan = 1;
+
+    UNLOCK;
+
+    return 1;
+}
+
+/*
+ * Note that the plan is to skip all tests
+ */
+int
+plan_skip_all(char *reason)
+{
+
+    LOCK;
+
+    _tap_init();
+
+    skip_all = 1;
+
+    printf("1..0");
+
+    if(reason != NULL)
+        printf(" # Skip %s", reason);
+
+    printf("\n");
+
+    UNLOCK;
+
+    exit(0);
+}
+
+/*
+ * Note the number of tests that will be run.
+ */
+int
+plan_tests(unsigned int tests)
+{
+
+    LOCK;
+
+    _tap_init();
+
+    if(have_plan != 0) {
+        fprintf(stderr, "You tried to plan twice!\n");
+        test_died = 1;
+        UNLOCK;
+        exit(255);
+    }
+
+    if(tests == 0) {
+        fprintf(stderr, "You said to run 0 tests!  You've got to run something.\n");
+        test_died = 1;
+        UNLOCK;
+        exit(255);
+    }
+
+    have_plan = 1;
+
+    _expected_tests(tests);
+
+    UNLOCK;
+
+    return e_tests;
+}
+
+unsigned int
+diag(char *fmt, ...)
+{
+    va_list ap;
+
+    fputs("# ", stderr);
+
+    va_start(ap, fmt);
+    vfprintf(stderr, fmt, ap);
+    va_end(ap);
+
+    fputs("\n", stderr);
+
+    return 0;
+}
+
+void
+_expected_tests(unsigned int tests)
+{
+
+    printf("1..%d\n", tests);
+    e_tests = tests;
+}
+
+int
+skip(unsigned int n, char *fmt, ...)
+{
+    va_list ap;
+    char *skip_msg;
+
+    LOCK;
+
+    va_start(ap, fmt);
+    asprintf(&skip_msg, fmt, ap);
+    va_end(ap);
+
+    while(n-- > 0) {
+        test_count++;
+        printf("ok %d # skip %s\n", test_count,
+               skip_msg != NULL ?
+               skip_msg : "libtap():malloc() failed");
+    }
+
+    free(skip_msg);
+
+    UNLOCK;
+
+    return 1;
+}
+
+void
+todo_start(char *fmt, ...)
+{
+    va_list ap;
+
+    LOCK;
+
+    va_start(ap, fmt);
+    vasprintf(&todo_msg, fmt, ap);
+    va_end(ap);
+
+    todo = 1;
+
+    UNLOCK;
+}
+
+void
+todo_end(void)
+{
+
+    LOCK;
+
+    todo = 0;
+    free(todo_msg);
+
+    UNLOCK;
+}
+
+int
+exit_status(void)
+{
+    int r;
+
+    LOCK;
+
+    /* If there's no plan, just return the number of failures */
+    if(no_plan || !have_plan) {
+        UNLOCK;
+        return failures;
+    }
+
+    /* Ran too many tests?  Return the number of tests that were run
+       that shouldn't have been */
+    if(e_tests < test_count) {
+        r = test_count - e_tests;
+        UNLOCK;
+        return r;
+    }
+
+    /* Return the number of tests that failed + the number of tests
+       that weren't run */
+    r = failures + e_tests - test_count;
+    UNLOCK;
+
+    return r;
+}
+
+/*
+ * Cleanup at the end of the run, produce any final output that might be
+ * required.
+ */
+void
+_cleanup(void)
+{
+
+    LOCK;
+
+    /* If plan_no_plan() wasn't called, and we don't have a plan,
+       and we're not skipping everything, then something happened
+       before we could produce any output */
+    if(!no_plan && !have_plan && !skip_all) {
+        diag("Looks like your test died before it could output anything.");
+        UNLOCK;
+        return;
+    }
+
+    if(test_died) {
+        diag("Looks like your test died just after %d.", test_count);
+        UNLOCK;
+        return;
+    }
+
+
+    /* No plan provided, but now we know how many tests were run, and can
+       print the header at the end */
+    if(!skip_all && (no_plan || !have_plan)) {
+        printf("1..%d\n", test_count);
+    }
+
+    if((have_plan && !no_plan) && e_tests < test_count) {
+        diag("Looks like you planned %d %s but ran %d extra.",
+             e_tests, e_tests == 1 ? "test" : "tests", test_count - e_tests);
+        UNLOCK;
+        return;
+    }
+
+    if((have_plan || !no_plan) && e_tests > test_count) {
+        diag("Looks like you planned %d %s but only ran %d.",
+             e_tests, e_tests == 1 ? "test" : "tests", test_count);
+        UNLOCK;
+        return;
+    }
+
+    if(failures)
+        diag("Looks like you failed %d %s of %d.",
+             failures, failures == 1 ? "test" : "tests", test_count);
+
+    UNLOCK;
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/src/tap.h
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/src/tap.h	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/src/tap.h	(revision 15358)
@@ -0,0 +1,89 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/* '## __VA_ARGS__' is a gcc'ism. C99 doesn't allow the token pasting
+   and requires the caller to add the final comma if they've ommitted
+   the optional arguments */
+#ifdef __GNUC__
+# define ok(e, test, ...) ((e) ?     \
+                           _gen_result(1, __func__, __FILE__, __LINE__, \
+                                       test, ## __VA_ARGS__) :  \
+                           _gen_result(0, __func__, __FILE__, __LINE__, \
+                                       test, ## __VA_ARGS__))
+
+# define ok1(e) ((e) ?       \
+                 _gen_result(1, __func__, __FILE__, __LINE__, "%s", #e) : \
+                 _gen_result(0, __func__, __FILE__, __LINE__, "%s", #e))
+
+# define pass(test, ...) ok(1, test, ## __VA_ARGS__);
+# define fail(test, ...) ok(0, test, ## __VA_ARGS__);
+
+# define skip_start(test, n, fmt, ...)   \
+do {      \
+    if((test)) {    \
+        skip(n, fmt, ## __VA_ARGS__); \
+        continue;   \
+    }
+    #elif __STDC_VERSION__ >= 199901L /* __GNUC__ */
+    # define ok(e, ...) ((e) ?      \
+                         _gen_result(1, __func__, __FILE__, __LINE__, \
+                                     __VA_ARGS__) :    \
+                         _gen_result(0, __func__, __FILE__, __LINE__, \
+                                     __VA_ARGS__))
+
+    # define ok1(e) ((e) ?       \
+                     _gen_result(1, __func__, __FILE__, __LINE__, "%s", #e) : \
+                     _gen_result(0, __func__, __FILE__, __LINE__, "%s", #e))
+
+    # define pass(...) ok(1, __VA_ARGS__);
+    # define fail(...) ok(0, __VA_ARGS__);
+
+    # define skip_start(test, n, ...)   \
+    do {      \
+        if((test)) {    \
+            skip(n,  __VA_ARGS__);  \
+            continue;   \
+        }
+        #else /* __STDC_VERSION__ */
+        # error "Needs gcc or C99 compiler for variadic macros."
+        #endif /* __STDC_VERSION__ */
+
+        #define skip_end() } while(0);
+
+    unsigned int _gen_result(int, const char *, char *, unsigned int, char *, ...);
+
+    int plan_no_plan(void);
+    int plan_skip_all(char *);
+    int plan_tests(unsigned int);
+
+    unsigned int diag(char *, ...);
+
+    int skip(unsigned int, char *, ...);
+
+    void todo_start(char *, ...);
+    void todo_end(void);
+
+    int exit_status(void);
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/.cvsignore	(revision 15358)
@@ -0,0 +1,6 @@
+Makefile
+Makefile.in
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/Makefile.am	(revision 15358)
@@ -0,0 +1,7 @@
+SUBDIRS=	diag
+SUBDIRS+=	fail
+SUBDIRS+=	ok
+SUBDIRS+=	pass
+SUBDIRS+=	plan
+SUBDIRS+=	skip
+SUBDIRS+=	todo
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/README
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/README	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/README	(revision 15358)
@@ -0,0 +1,12 @@
+Most of the tests follow the same pattern.
+
+ * test.pl that uses Test::More, and demonstrates whatever functionality 
+   that we're trying to test.  This is the reference code.
+
+ * test.c, which tests the libtap reimplementation of the same functionality.
+
+ * test.t, which compiles the .c program, runs both test scripts, and then 
+   diffs their output to make sure it's identical.
+
+   Right now, test.t is identical in every directory.  This sucks somewhat.
+   It should either be a symlink to a common script
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/diag/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/diag/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/diag/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/diag/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/diag/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/diag/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/diag/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/diag/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/diag/test.c	(revision 15358)
@@ -0,0 +1,46 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    plan_tests(2);
+
+    rc = diag("A diagnostic message");
+    diag("Returned: %d", rc);
+
+    /* Make sure the failure is passed through */
+    ok(1, "test 1") || diag("ok() failed, and shouldn't");
+    ok(0, "test 2") || diag("ok() passed, and shouldn't");
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/diag/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/diag/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/diag/test.pl	(revision 15358)
@@ -0,0 +1,16 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+plan tests => 2;
+
+$rc = diag("A diagnostic message");
+diag("Returned: $rc");
+
+ok(1, 'test 1') or diag "ok() failed, and shouldn't";
+ok(0, 'test 2') or diag "ok() passed, and shouldn't";
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/diag/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/diag/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/diag/test.t	(revision 15358)
@@ -0,0 +1,67 @@
+#!/bin/sh
+
+echo '1..7'
+
+perl $srcdir/test.pl 2>/dev/null >test.pl.out
+perlstatus=$?
+
+./test > test.c.out 2>&1
+cstatus=$?
+
+if grep "^# A diagnostic message$" test.c.out > /dev/null ; then
+    echo "ok 1 - found a diagnostic message"
+else
+    echo "not ok 1 - found a diagnostic message"
+    retval=1
+fi
+
+if grep "^# Returned: 0$" test.c.out > /dev/null ; then
+    echo "ok 2 - diag() expected return value" 
+else
+    echo "not ok 2 - diag() expected return value" 
+    retval=1
+fi
+
+if grep "^#     Failed test (.*test.c:main() at line 43)$" test.c.out > /dev/null ; then
+    echo "ok 3 - 'failed test at line' diag" 
+else
+    echo "not ok 3 - 'failed test at line' diag" 
+    retval=1
+fi
+
+if grep "^# ok() passed, and shouldn't$" test.c.out > /dev/null ; then
+    echo "ok 4 - expected diag"
+else
+    echo "ok 4 - expected diag"
+    retval=1
+fi
+
+if grep "^# Looks like you failed 1 test of 2.$" test.c.out > /dev/null ; then
+    echo "ok 5 - failed 1 test"
+ else
+    echo "ok 5 - failed 1 test"
+    retval=1
+fi
+
+sed -e '/^#/D' test.c.out > tmp
+mv tmp test.c.out
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 6 - TAP output is identical'
+else
+	retval=1
+	echo 'not ok 6 - TAP output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 7 - status code'
+else
+	retval=1
+	echo 'not ok 7 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/fail/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/fail/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/fail/.cvsignore	(revision 15358)
@@ -0,0 +1,15 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/fail/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/fail/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/fail/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/fail/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/fail/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/fail/test.c	(revision 15358)
@@ -0,0 +1,46 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_tests(2);
+    diag("Returned: %d", rc);
+
+    rc = fail("test to fail");
+    diag("Returned: %d", rc);
+
+    rc = fail("test to fail %s", "with extra string");
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/fail/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/fail/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/fail/test.pl	(revision 15358)
@@ -0,0 +1,17 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 2;
+diag("Returned: " . sprintf('%d', $rc));
+
+$rc = fail('test to fail');
+diag("Returned: $rc");
+
+$rc = fail('test to fail with extra string');
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/fail/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/fail/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/fail/test.t	(revision 15358)
@@ -0,0 +1,74 @@
+#!/bin/sh
+
+echo '1..8'
+
+perl $srcdir/test.pl 2>/dev/null > test.pl.out
+perlstatus=$?
+
+./test > test.c.out 2>&1 
+cstatus=$?
+
+if grep "^# Returned: 2$" test.c.out >/dev/null ; then
+  echo "ok 1 - expected return value";
+else
+  echo "not ok 1 - expected return value";
+  retval=1
+fi
+
+if grep "^#     Failed test (.*test.c:main() at line 39)$" test.c.out >/dev/null ; then
+  echo "ok 2 - failed expected test";
+else
+  echo "not ok 2 - failed expected test";
+  retval=1
+fi
+
+if grep "^# Returned: 0$" test.c.out >/dev/null ; then
+  echo "ok 3 - expected return value";
+else
+  echo "not ok 3 - expected return value";
+  retval=1
+fi
+
+if grep "^#     Failed test (.*test.c:main() at line 42)$" test.c.out >/dev/null ; then
+  echo "ok 4 - failed expected test";
+else
+  echo "not ok 4 - failed expected test";
+  retval=1
+fi
+  
+if grep "^# Returned: 0$" test.c.out >/dev/null ; then
+  echo "ok 5 - expected return value";
+else
+  echo "not ok 5 - expected return value";
+  retval=1
+fi
+
+if grep "^# Looks like you failed 2 tests of 2.$" test.c.out >/dev/null ; then
+  echo "ok 6 - expected return value";
+else
+  echo "not ok 6 - expected return value";
+  retval=1
+fi
+
+sed -e '/^#/D' test.c.out > tmp
+mv tmp test.c.out
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 7 - output is identical'
+else
+	retval=1
+	echo 'not ok 7 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 8 - status code'
+else
+	retval=1
+	echo 'not ok 8 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/.cvsignore	(revision 15358)
@@ -0,0 +1,6 @@
+Makefile
+Makefile.in
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/Makefile.am	(revision 15358)
@@ -0,0 +1,3 @@
+SUBDIRS  =	ok
+SUBDIRS +=	ok-hash
+SUBDIRS +=	ok-numeric
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/test.c	(revision 15358)
@@ -0,0 +1,52 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_tests(4);
+    diag("Returned: %d", rc);
+
+    rc = ok(1, "Test with no hash");
+    diag("Returned: %d", rc);
+
+    rc = ok(1, "Test with one # hash");
+    diag("Returned: %d", rc);
+
+    rc = ok(1, "Test with # two # hashes");
+    diag("Returned: %d", rc);
+
+    rc = ok(1, "Test with ## back to back hashes");
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/test.pl	(revision 15358)
@@ -0,0 +1,24 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 4;
+diag("Returned: " . sprintf("%d", $rc));
+
+
+$rc = ok(1, 'Test with no hash');
+diag("Returned: $rc");
+
+$rc = ok(1, 'Test with one # hash');
+diag("Returned: $rc");
+
+$rc = ok(1, 'Test with # two # hashes');
+diag("Returned: $rc");
+
+$rc = ok(1, 'Test with ## back to back hashes');
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-hash/test.t	(revision 15358)
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/test.c	(revision 15358)
@@ -0,0 +1,49 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_tests(3);
+    diag("Returned: %d", rc);
+
+    rc = ok(1, "First test");
+    diag("Returned: %d", rc);
+
+    rc = ok(1, "1");
+    diag("Returned: %d", rc);
+
+    rc = ok(1, "Third test");
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/test.pl	(revision 15358)
@@ -0,0 +1,21 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 3;
+diag("Returned: " . sprintf("%d", $rc));
+
+
+$rc = ok(1, 'First test');
+diag("Returned: $rc");
+
+$rc = ok(1, '1');
+diag("Returned: $rc");
+
+$rc = ok(1, 'Third test');
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok-numeric/test.t	(revision 15358)
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/test.c	(revision 15358)
@@ -0,0 +1,55 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_tests(5);
+    diag("Returned: %d", rc);
+
+    rc = ok(1 == 1, "1 equals 1");
+    diag("Returned: %d", rc);
+
+    rc = ok(1 == 1, "1 equals %d", 1);
+    diag("Returned: %d", rc);
+
+    rc = ok1(1 == 1);
+    diag("Returned: %d", rc);
+
+    rc = ok(1 == 2, "1 equals 2");
+    diag("Returned: %d", rc);
+
+    rc = ok1(1 == 2);
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/test.pl	(revision 15358)
@@ -0,0 +1,27 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 5;
+diag("Returned: " . sprintf("%d", $rc));
+
+
+$rc = ok(1 == 1, '1 equals 1');	# Test ok() passes when it should
+diag("Returned: $rc");
+
+$rc = ok(1 == 1, '1 equals 1'); # Used for %d testing in test.c
+diag("Returned: $rc");
+
+$rc = ok(1 == 1, '1 == 1');	# Test ok1() passes when it should
+diag("Returned: $rc");
+
+$rc = ok(1 == 2, '1 equals 2');	# Test ok() fails when it should
+diag("Returned: $rc");
+
+$rc = ok(1 == 2, '1 == 2');	# Test ok1() fails when it should
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/ok/ok/test.t	(revision 15358)
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/pass/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/pass/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/pass/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/pass/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/pass/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/pass/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/pass/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/pass/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/pass/test.c	(revision 15358)
@@ -0,0 +1,46 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_tests(2);
+    diag("Returned: %d", rc);
+
+    rc = pass("test to pass");
+    diag("Returned: %d", rc);
+
+    rc = pass("test to pass %s", "with extra string");
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/pass/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/pass/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/pass/test.pl	(revision 15358)
@@ -0,0 +1,17 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 2;
+diag("Returned: " . sprintf('%d', $rc));
+
+$rc = pass('test to pass');
+diag("Returned: $rc");
+
+$rc = pass('test to pass with extra string');
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/pass/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/pass/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/pass/test.t	(revision 15358)
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/.cvsignore	(revision 15358)
@@ -0,0 +1,6 @@
+Makefile
+Makefile.in
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/Makefile.am	(revision 15358)
@@ -0,0 +1,7 @@
+SUBDIRS  =	no-tests
+SUBDIRS +=	no_plan
+SUBDIRS +=	not-enough-tests
+SUBDIRS +=	too-many-plans
+SUBDIRS +=	too-many-tests
+SUBDIRS +=	sane
+SUBDIRS +=	skip_all
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/test.c	(revision 15358)
@@ -0,0 +1,43 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_tests(0);
+    diag("Returned: %d", rc);
+
+    rc = ok(1, NULL);
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/test.pl	(revision 15358)
@@ -0,0 +1,14 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 0;
+diag("Returned: " . sprintf("%d", $rc));
+
+$rc = ok(1);
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no-tests/test.t	(revision 15358)
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/test.c	(revision 15358)
@@ -0,0 +1,43 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_no_plan();
+    diag("Returned: %d", rc);
+
+    rc = ok(1, NULL);
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/test.pl	(revision 15358)
@@ -0,0 +1,14 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+my $rc = 0;
+
+use Test::More;
+
+$rc = plan qw(no_plan);
+diag("Returned: " . sprintf("%d", $rc));
+
+$rc = ok(1);
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/no_plan/test.t	(revision 15358)
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/test.c	(revision 15358)
@@ -0,0 +1,49 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_tests(1);
+    diag("Returned: %d", rc);
+
+    rc = ok(1, NULL);
+    diag("Returned: %d", rc);
+
+    rc = ok(1, NULL);
+    diag("Returned: %d", rc);
+
+    rc = ok(1, NULL);
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/test.pl	(revision 15358)
@@ -0,0 +1,20 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 1;
+diag("Returned: " . sprintf("%d", $rc));
+
+$rc = ok(1);
+diag("Returned: $rc");
+
+$rc = ok(1);
+diag("Returned: $rc");
+
+$rc = ok(1);
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/not-enough-tests/test.t	(revision 15358)
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	# comment this out until we're exit-code compatible with Test::More
+	#retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/test.c	(revision 15358)
@@ -0,0 +1,43 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_tests(1);
+    diag("Returned: %d", rc);
+
+    rc = ok(1, NULL);
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/test.pl	(revision 15358)
@@ -0,0 +1,14 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 1;
+diag("Returned: " . sprintf("%d", $rc));
+
+$rc = ok(1);
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/sane/test.t	(revision 15358)
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/plan.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/plan.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/plan.c	(revision 15358)
@@ -0,0 +1,39 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "tap.h"
+
+/* Run pre-defined tests on the test library to make sure that the basic
+   functionality works, and it can be used to test itself afterwards */
+
+int
+main(int argc, char *argv[])
+{
+    plan_skip_all("No good reason");
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/test.c	(revision 15358)
@@ -0,0 +1,38 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_skip_all("No good reason");
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/test.pl	(revision 15358)
@@ -0,0 +1,11 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan skip_all => "No good reason";
+diag("Returned: " . sprintf("%d", $rc));
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/skip_all/test.t	(revision 15358)
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/test.c	(revision 15358)
@@ -0,0 +1,49 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_tests(1);
+    diag("Returned: %d", rc);
+
+    rc = ok(1, NULL);
+    diag("Returned: %d", rc);
+
+    rc = plan_tests(1);
+    diag("Returned: %d", rc);
+
+    rc = ok(0, NULL);
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/test.pl	(revision 15358)
@@ -0,0 +1,20 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 1;
+diag("Returned: " . sprintf("%d", $rc));
+
+$rc = ok(1);
+diag("Returned: $rc");
+
+$rc = plan tests => 1;
+diag("Returned: $rc");
+
+$rc = ok(0);
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-plans/test.t	(revision 15358)
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/test.c	(revision 15358)
@@ -0,0 +1,46 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+
+    rc = plan_tests(5);
+    diag("Returned: %d", rc);
+
+    rc = ok(1, NULL);
+    diag("Returned: %d", rc);
+
+    rc = ok(0, NULL);
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/test.pl	(revision 15358)
@@ -0,0 +1,17 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 5;
+diag("Returned: " . sprintf("%d", $rc));
+
+$rc = ok(1);
+diag("Returned: $rc");
+
+$rc = ok(0);
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/plan/too-many-tests/test.t	(revision 15358)
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+    # we're not exit-status compatible with Test::More yet
+	#retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/skip/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/skip/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/skip/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/skip/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/skip/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/skip/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/skip/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/skip/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/skip/test.c	(revision 15358)
@@ -0,0 +1,69 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+    unsigned int side_effect = 0;
+
+    rc = plan_tests(4);
+    diag("Returned: %d", rc);
+
+    rc = ok(1 == 1, "1 equals 1"); /* Should always work */
+    diag("Returned: %d", rc);
+
+    do {
+        if(1) {
+            rc = skip(1, "Testing skipping");
+            continue;
+        }
+
+        side_effect++;
+
+        ok(side_effect == 1, "side_effect checked out");
+
+    } while(0);
+
+    diag("Returned: %d", rc);
+
+    skip_start(1 == 1, 1, "Testing skipping #2");
+
+    side_effect++;
+    rc = ok(side_effect == 1, "side_effect checked out");
+    diag("Returned: %d", rc);
+
+    skip_end();
+
+    rc = ok(side_effect == 0, "side_effect is %d", side_effect);
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/skip/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/skip/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/skip/test.pl	(revision 15358)
@@ -0,0 +1,40 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 4;
+diag("Returned: " . sprintf("%d", $rc));
+
+my $side_effect = 0;		# Check whether skipping has side effects
+
+$rc = ok(1 == 1, '1 equals 1');	# Test ok() passes when it should
+diag("Returned: $rc");
+
+# Start skipping
+SKIP: {
+	$rc = skip "Testing skipping", 1;
+
+	$side_effect++;
+
+	$rc = ok($side_effect == 1, '$side_effect checked out');
+}
+
+diag("Returned: $rc");
+
+SKIP: {
+	$rc = skip "Testing skipping #2", 1;
+	diag("Returned: $rc");
+
+	$side_effect++;
+
+	$rc = ok($side_effect == 1, '$side_effect checked out');
+	diag("Returned: $rc");
+}
+
+$rc = ok($side_effect == 0, "side_effect is $side_effect");
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/skip/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/skip/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/skip/test.t	(revision 15358)
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/todo/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/todo/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/todo/.cvsignore	(revision 15358)
@@ -0,0 +1,11 @@
+.deps
+Makefile
+Makefile.in
+.libs
+test
+test.c.out
+test.pl.out
+*.bb
+*.bbg
+*.da
+gmon.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/todo/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/todo/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/todo/Makefile.am	(revision 15358)
@@ -0,0 +1,13 @@
+
+TESTS = 		test.t
+TESTS_ENVIRONMENT =	$(SHELL)
+
+EXTRA_DIST = 		$(TESTS) test.pl
+
+check_PROGRAMS = 	test
+
+test_CFLAGS = 		-g -I$(top_srcdir)/src
+test_LDFLAGS = 		-L$(top_builddir)/src
+test_LDADD = 		-ltap
+
+CLEANFILES =	test.o test.c.out test.pl.out
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/todo/test.c
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/todo/test.c	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/todo/test.c	(revision 15358)
@@ -0,0 +1,68 @@
+/*-
+ * Copyright (c) 2004 Nik Clayton
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "tap.h"
+
+int
+main(int argc, char *argv[])
+{
+    unsigned int rc = 0;
+    unsigned int side_effect = 0;
+
+    rc = plan_tests(5);
+    diag("Returned: %d", rc);
+
+    rc = ok(1 == 1, "1 equals 1"); /* Should always work */
+    diag("Returned: %d", rc);
+
+    todo_start("For testing purposes");
+
+    side_effect++;
+
+    /* This test should fail */
+    rc = ok(side_effect == 0, "side_effect checked out");
+    diag("Returned: %d", rc);
+
+    /* This test should unexpectedly succeed */
+    rc = ok(side_effect == 1, "side_effect checked out");
+    diag("Returned: %d", rc);
+
+    todo_end();
+
+    todo_start("Testing printf() %s in todo_start()", "expansion");
+
+    rc = ok(0, "dummy test");
+    diag("Returned: %d", rc);
+
+    todo_end();
+
+    rc = ok(side_effect == 1, "side_effect is %d", side_effect);
+    diag("Returned: %d", rc);
+
+    return exit_status();
+}
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/todo/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/todo/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/todo/test.pl	(revision 15358)
@@ -0,0 +1,41 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Test::More;
+
+my $rc = 0;
+
+$rc = plan tests => 5;
+diag("Returned: " . sprintf("%d", $rc));
+
+my $side_effect = 0;		# Check whether TODO has side effects
+
+$rc = ok(1 == 1, '1 equals 1');	# Test ok() passes when it should
+diag("Returned: $rc");
+
+# Start TODO tests
+TODO: {
+	local $TODO = 'For testing purposes';
+
+	$side_effect++;
+
+	# This test should fail
+	$rc = ok($side_effect == 0, 'side_effect checked out');
+	diag("Returned: $rc");
+
+	# This test should unexpectedly succeed
+	$rc = ok($side_effect == 1, 'side_effect checked out');
+	diag("Returned: $rc");
+}
+
+TODO: {
+	local $TODO = 'Testing printf() expansion in todo_start()';
+
+	$rc = ok(0, 'dummy test');
+	diag("Returned: $rc");
+}
+
+$rc = ok($side_effect == 1, "side_effect is $side_effect");
+diag("Returned: $rc");
Index: /branches/eam_branch_20071023/psModules/test/tap/tests/todo/test.t
===================================================================
--- /branches/eam_branch_20071023/psModules/test/tap/tests/todo/test.t	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/tap/tests/todo/test.t	(revision 15358)
@@ -0,0 +1,34 @@
+#!/bin/sh
+
+echo '1..2'
+
+perl $srcdir/test.pl 2> /dev/null > test.pl.out
+perlstatus=$?
+
+# Test:;More prints diagnostic from TODO tests on stdout
+# http://rt.cpan.org/Ticket/Display.html?id=14982
+sed '/^#/D' test.pl.out > tmp
+mv tmp test.pl.out
+
+./test 2> /dev/null > test.c.out
+cstatus=$?
+
+diff -u test.pl.out test.c.out
+
+if [ $? -eq 0 ]; then
+	echo 'ok 1 - output is identical'
+else
+	retval=1
+	echo 'not ok 1 - output is identical'
+fi
+
+if [ $perlstatus -eq $cstatus ]; then
+	echo 'ok 2 - status code'
+else
+	retval=1
+	echo 'not ok 2 - status code'
+	echo "# perlstatus = $perlstatus"
+	echo "#    cstatus = $cstatus"
+fi
+
+exit $retval
Index: /branches/eam_branch_20071023/psModules/test/test.pl
===================================================================
--- /branches/eam_branch_20071023/psModules/test/test.pl	(revision 15358)
+++ /branches/eam_branch_20071023/psModules/test/test.pl	(revision 15358)
@@ -0,0 +1,31 @@
+#!/usr/bin/env perl
+
+# Copyright (C) 2006  Joshua Hoblitt
+#
+# $Id: test.pl,v 1.1 2006-09-23 02:47:58 magnier Exp $
+
+use strict;
+use warnings FATAL => qw( all);
+
+use vars qw($VERSION);
+$VERSION = '0.01';
+
+use File::Find::Rule;
+use Cwd;
+
+my $rule = File::Find::Rule->new;
+# ignore .lib directories
+$rule->or($rule->new
+        ->directory
+        ->name('.libs')
+        ->prune
+        ->discard,
+        $rule->new
+    );
+$rule->name(qr/^tap_[^.]*$/)
+        ->maxdepth(2)
+        ->relative;
+
+my @test_files = $rule->in(getcwd());
+
+system("prove @test_files");
Index: /branches/eam_branch_20071023/psastro/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psastro/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/.cvsignore	(revision 15358)
@@ -0,0 +1,18 @@
+bin
+Makefile
+Makefile.in
+aclocal.m4
+autom4te.cache
+compile
+config.log
+config.status
+configure
+depcomp
+install-sh
+missing
+config.guess
+libtool
+ltmain.sh
+stamp-h1
+config.sub
+psastro.pc
Index: /branches/eam_branch_20071023/psastro/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psastro/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/Makefile.am	(revision 15358)
@@ -0,0 +1,10 @@
+SUBDIRS = src
+
+CLEANFILES = *.pyc *~ core core.*
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA= psastro.pc
+
+EXTRA_DIST = \
+	psastro.pc.in \
+	autogen.sh
Index: /branches/eam_branch_20071023/psastro/autogen.sh
===================================================================
--- /branches/eam_branch_20071023/psastro/autogen.sh	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/autogen.sh	(revision 15358)
@@ -0,0 +1,103 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+srcdir=`dirname $0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+cd $srcdir
+
+PROJECT=psastro
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL="aclocal $ACLOCAL_FLAGS"
+AUTOHEADER=autoheader
+AUTOMAKE=automake
+AUTOCONF=autoconf
+
+($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $LIBTOOlIZE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/libtool/"
+        DIE=1
+}
+
+($ACLOCAL --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $ACLOCAL installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOHEADER --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOHEADER installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOMAKE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOCONF --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOCONF installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+if test "$DIE" -eq 1; then
+        exit 1
+fi
+
+test $TEST_TYPE $FILE || {
+        echo "You must run this script in the top-level $PROJECT directory"
+        exit 1
+}
+
+if test -z "$*"; then
+        echo "I am going to run ./configure with no arguments - if you wish "
+        echo "to pass any to it, please specify them on the $0 command line."
+fi
+
+$LIBTOOLIZE --copy --force || echo "$LIBTOOlIZE failed"
+$ACLOCAL || echo "$ACLOCAL failed"
+$AUTOHEADER || echo "$AUTOHEADER failed"
+$AUTOMAKE --add-missing --force-missing --copy || echo "$AUTOMAKE  failed"
+$AUTOCONF || echo "$AUTOCONF failed"
+
+cd $ORIGDIR
+
+run_configure=true
+for arg in $*; do
+    case $arg in
+        --no-configure)
+            run_configure=false
+            ;;
+        *)
+            ;;
+    esac
+done
+
+if $run_configure; then
+    $srcdir/configure --enable-maintainer-mode "$@"
+    echo
+    echo "Now type 'make' to compile $PROJECT."
+else
+    echo
+    echo "Now run 'configure' and 'make' to compile $PROJECT."
+fi
Index: /branches/eam_branch_20071023/psastro/configure.ac
===================================================================
--- /branches/eam_branch_20071023/psastro/configure.ac	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/configure.ac	(revision 15358)
@@ -0,0 +1,203 @@
+dnl Process this file with autoconf to produce a configure script.
+AC_PREREQ(2.61)
+
+AC_INIT([psastro], [0.9.0], [ipp-support@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([src])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+AM_CONFIG_HEADER([src/config.h])
+AM_MAINTAINER_MODE
+
+IPP_STDCFLAGS
+
+AC_LANG(C)
+AC_GNU_SOURCE
+AC_PROG_CC_C99
+AC_PROG_INSTALL
+AC_PROG_LIBTOOL
+AC_SYS_LARGEFILE
+
+dnl ------------------------------------------------------------
+
+AC_PATH_PROG([ERRORCODES], [psParseErrorCodes], [missing])
+if test "$ERRORCODES" = "missing" ; then
+  AC_MSG_ERROR([psParseErrorCodes is required])
+fi
+
+dnl ------------------ kapa,libkapa options -------------------------
+dnl -- libkapa implies the requirement for libpng, libjpeg as well --
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+dnl test for command-line options: use ohana-config if not supplied
+KAPA_CFLAGS_CONFIG="true"
+KAPA_LIBS_CONFIG="true"
+AC_ARG_WITH(kapa,
+[AS_HELP_STRING(--with-kapa=DIR,Specify location of libkapa)],
+[KAPA_CFLAGS="-I$withval/include" KAPA_LIBS="-L$withval/lib" 
+ KAPA_CFLAGS_CONFIG="false"       KAPA_LIBS_CONFIG="false"])
+AC_ARG_WITH(kapa-include,
+[AS_HELP_STRING(--with-kapa-include=DIR,Specify libkapa include directory.)],
+[KAPA_CFLAGS="-I$withval" KAPA_CFLAGS_CONFIG="false"])
+AC_ARG_WITH(kapa-lib,
+[AS_HELP_STRING(--with-kapa-lib=DIR,Specify libkapa library directory.)],
+[KAPA_LIBS="-L$withval" KAPA_LIBS_CONFIG="false"])
+
+echo "KAPA_CFLAGS_CONFIG: $KAPA_CFLAGS_CONFIG"
+echo "KAPA_LIBS_CONFIG: $KAPA_LIBS_CONFIG"
+echo "KAPA_CFLAGS: $KAPA_CFLAGS"
+echo "KAPA_LIBS: $KAPA_LIBS"
+
+dnl HAVE_KAPA is set to false if any of the tests fail
+HAVE_KAPA="true"
+AC_MSG_NOTICE([checking for libkapa])
+if test "$KAPA_CFLAGS_CONFIG" = "true" -o "$KAPA_LIBS_CONFIG" = "true"; then
+  AC_MSG_NOTICE([kapa info supplied by ohana-config])
+  KAPA_CONFIG=`which ohana-config`
+  AC_CHECK_FILE($KAPA_CONFIG,[],
+    [HAVE_KAPA="false"; AC_MSG_WARN([libkapa is not found: output plots disabled.  Obtain libkapa at http://kiawe.ifa.hawaii.edu/Elixir/Ohana or use --with-kapa to specify location])])
+  
+  echo "HAVE_KAPA: $HAVE_KAPA"
+  echo "KAPA_CFLAGS_CONFIG: $KAPA_CFLAGS_CONFIG"
+
+  if test "$HAVE_KAPA" = "true" -a "$KAPA_CFLAGS_CONFIG" = "true" ; then
+   AC_MSG_NOTICE([libkapa cflags info supplied by ohana-config])
+   AC_MSG_CHECKING([libkapa cflags])
+   KAPA_CFLAGS="`${KAPA_CONFIG} --cflags`"
+   AC_MSG_RESULT([${KAPA_CFLAGS}])
+  fi
+
+  if test "$HAVE_KAPA" = "true" -a "$KAPA_LIBS_CONFIG" = "true" ; then
+   AC_MSG_NOTICE([libkapa ldflags info supplied by ohana-config])
+   AC_MSG_CHECKING([libkapa ldflags])
+   KAPA_LIBS="`${KAPA_CONFIG} --libs` -lX11"
+   AC_MSG_RESULT([${KAPA_LIBS}])
+  fi
+fi
+
+if test "$HAVE_KAPA" = "true" ; then
+ AC_MSG_NOTICE([libkapa supplied])
+ PSASTRO_CFLAGS="${PSASTRO_CFLAGS} ${KAPA_CFLAGS}"
+ PSASTRO_LIBS="${PSASTRO_LIBS} ${KAPA_LIBS}"
+else
+ AC_MSG_NOTICE([libkapa ignored])
+fi
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ libjpeg options ---------------------
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+AC_ARG_WITH(jpeg,
+[AS_HELP_STRING(--with-jpeg=DIR,Specify location of libjpeg.)],
+[JPEG_CFLAGS="-I$withval/include"
+ JPEG_LDFLAGS="-L$withval/lib"])
+AC_ARG_WITH(jpeg-include,
+[AS_HELP_STRING(--with-jpeg-include=DIR,Specify libjpeg include directory.)],
+[JPEG_CFLAGS="-I$withval"])
+AC_ARG_WITH(jpeg-lib,
+[AS_HELP_STRING(--with-jpeg-lib=DIR,Specify libjpeg library directory.)],
+[JPEG_LDFLAGS="-L$withval"])
+
+CFLAGS="${CFLAGS} ${JPEG_CFLAGS}"
+CPPFLAGS=${CFLAGS}
+LDFLAGS="${LDFLAGS} ${JPEG_LDFLAGS}"
+
+AC_CHECK_HEADERS([jpeglib.h],
+  [PSASTRO_CFLAGS="$PSASTRO_CFLAGS $JPEG_CFLAGS" AC_SUBST(JPEG_CFLAGS)],
+  [HAVE_KAPA=false; AC_MSG_WARN([libjpeg headers not found: output plots disabled.  Obtain libjpeg from http://www.ijg.org/ or use --with-jpeg to specify location.])]
+)
+
+AC_CHECK_LIB(jpeg,jpeg_CreateCompress,
+  [PSASTRO_LIBS="$PSASTRO_LIBS $JPEG_LDFLAGS -ljpeg"],
+  [HAVE_KAPA=false; AC_MSG_WARN([libjpeg library not found: output plots disabled.  Obtain libjpeg from http://www.ijg.org/ or use --with-jpeg to specify location.])]
+)
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ libpng options ---------------------
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+AC_ARG_WITH(png,
+[AS_HELP_STRING(--with-png=DIR,Specify location of libpng.)],
+[PNG_CFLAGS="-I$withval/include"
+ PNG_LDFLAGS="-L$withval/lib"])
+AC_ARG_WITH(png-include,
+[AS_HELP_STRING(--with-png-include=DIR,Specify libpng include directory.)],
+[PNG_CFLAGS="-I$withval"])
+AC_ARG_WITH(png-lib,
+[AS_HELP_STRING(--with-png-lib=DIR,Specify libpng library directory.)],
+[PNG_LDFLAGS="-L$withval"])
+
+CFLAGS="${CFLAGS} ${PNG_CFLAGS}"
+CPPFLAGS=${CFLAGS}
+LDFLAGS="${LDFLAGS} ${PNG_LDFLAGS}"
+
+AC_CHECK_HEADERS([png.h],
+  [PSASTRO_CFLAGS="$PSASTRO_CFLAGS $PNG_CFLAGS" AC_SUBST(PNG_CFLAGS)],
+  [HAVE_KAPA=false; AC_MSG_WARN([libpng headers not found: output plots disabled.  Obtain libpng from http://www.ijg.org/ or use --with-png to specify location.])]
+)
+
+AC_CHECK_LIB(png,png_init_io,
+  [PSASTRO_LIBS="$PSASTRO_LIBS $PNG_LDFLAGS -lpng"],
+  [HAVE_KAPA=false; AC_MSG_WARN([libpng library not found: output plots disabled.  Obtain libpng from http://www.ijg.org/ or use --with-png to specify location.])]
+)
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ use kapa or not? ---------------------
+
+if test "$HAVE_KAPA" == "true" ; then
+  AC_MSG_RESULT([including plotting functions])
+  AC_DEFINE([HAVE_KAPA],[1],[enable use of libkapa])
+else
+  AC_MSG_RESULT([skipping plotting functions])
+  AC_DEFINE([HAVE_KAPA],[0],[disable use of libkapa])
+fi
+
+dnl ------------- psLib, psModules ---------------
+PKG_CHECK_MODULES([PSLIB], [pslib >= 1.0.0])
+PKG_CHECK_MODULES([PSMODULE], [psmodules >= 1.0.0])
+
+dnl Set CFLAGS for build
+IPP_STDOPTS
+CFLAGS="${CFLAGS=} -Wall -Werror"
+echo "PSASTRO_CFLAGS: $PSASTRO_CFLAGS"
+echo "PSASTRO_LIBS: $PSASTRO_LIBS"
+
+AC_SUBST([PSASTRO_CFLAGS])
+AC_SUBST([PSASTRO_LIBS])
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+  psastro.pc
+])
+
+AC_OUTPUT
Index: /branches/eam_branch_20071023/psastro/doc/mktest.txt
===================================================================
--- /branches/eam_branch_20071023/psastro/doc/mktest.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/doc/mktest.txt	(revision 15358)
@@ -0,0 +1,31 @@
+
+the program, psastro-mktest, will generate a set of fake test data for
+psastro.  the config file 'psastro-mktest.conf' gives an example setup
+for this program.  It generates 4 output files:
+
+- cmpfile : this is a basic elixir-style cmp file (stars plus header)
+  which can be taken as input to psastro
+
+- catfile : this is a basic text reference catalog, which the current
+  psastro loads as a reference
+
+- rawfile : this is a text listing of all astrometry stages for the
+  generated stars: readout, cell, chip, fpa, tpa, sky, with the last
+  two columns being the magnitudes.  this file is generated by
+  applying the transformations starting at the readout and going
+  step-by-step to the sky.
+
+- reffile : this is the inverse of the preceeding file, with the
+  resulting sources transformed from the sky coordinates down
+  step-by-step to the readout pixels.  If the transformations are
+  functioning, these two files (rawfile & reffile) should be identical
+  within floating point errors.
+
+examples:
+
+build a fake dataset
+# psastro-mktest doc/psastro-mktest.conf test.cmp test.cat test.raw test.ref
+
+run psastrom on the fake data:
+# psastro doc/psastro.conf test.cmp test.dat
+
Index: /branches/eam_branch_20071023/psastro/doc/notes.txt
===================================================================
--- /branches/eam_branch_20071023/psastro/doc/notes.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/doc/notes.txt	(revision 15358)
@@ -0,0 +1,39 @@
+
+2006.12.26
+
+ work still to be done for psastro:
+
+ - test suite
+   - create a DVO database from a fake population
+   - create fake images with range of errors drawn from DVO
+
+ - 
+
+
+we have a few different astrometry circumstances for our complete
+image/chip hierarchy structure:
+
+- ChipAstrom with one readout, one cell per chip:
+
+  whether or not there is more than one chip, we need to have two
+  stages
+  
+    - in the first stage, the per-chip results are applied to the
+      chip.toFPA parameter set.  
+
+    - in the second stage, the chip results are collectively used to
+      modify the fpa.toSky terms.
+
+      - the average offsets of the chip positions relative to starting
+	coordinates are used to calculate a single average offset to
+	the boresite. 
+
+      - the average rotations of the chips relative to their starting
+        rotations are used to calculate a single average rotation of
+        the boresite
+
+
+- ChipAstrom with more than one readout and/or cell:
+
+  - match all stars up relative to first readout?
+   
Index: /branches/eam_branch_20071023/psastro/doc/psastro-mktest.conf
===================================================================
--- /branches/eam_branch_20071023/psastro/doc/psastro-mktest.conf	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/doc/psastro-mktest.conf	(revision 15358)
@@ -0,0 +1,28 @@
+
+NX            S32  2000    	 # chip dimensions
+NY            S32  2000    	 # chip dimensions
+
+NSTARS        S32  20    	 # number of fake stars to generate
+
+# the current test output file only has the basic WCS info.  
+READOUT.COL.0    F32  0		 # cell coord for readout (0,0)
+READOUT.ROW.0    F32  0		 # cell coord for readout (0,0)
+READOUT.COL.BIN  F32  1		 # readout col binning factor
+READOUT.ROW.BIN  F32  1		 # readout row binning factor
+
+CELL.COL.0    	 F32  0		 # chip coord for cell (0,0)
+CELL.ROW.0    	 F32  0		 # chip coord for cell (0,0)
+
+CHIP.COL.0    	 F32  0		 # FPA coord for chip (0,0)
+CHIP.ROW.0    	 F32  0		 # FPA coord for chip (0,0)
+CHIP.THETA    	 F32  0	         # cw rotation from chip to FPA
+CHIP.PARITY.X 	 F32  1		 # x flip 
+CHIP.PARITY.Y 	 F32  1		 # y flip 
+
+FPA.THETA     	 F32  0		 # fpa row offset
+FPA.COL.0  	 F32  0		 # TPA coord for FPA (0,0)
+FPA.ROW.0  	 F32  0		 # TPA coord for FPA (0,0)
+
+PLATE.SCALE	 F32  1	         # plate scale in arcsec/pixel
+RA		 F32  10	 # celestial coord for TPA (0,0)
+DEC     	 F32  20	 # celestial coord for TPA (0,0)
Index: /branches/eam_branch_20071023/psastro/doc/psastro.conf
===================================================================
--- /branches/eam_branch_20071023/psastro/doc/psastro.conf	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/doc/psastro.conf	(revision 15358)
@@ -0,0 +1,17 @@
+
+PSASTRO.MATCH.RADIUS 	F32 2.0    # fitting radius in pixels
+
+PSASTRO.CHIP.NX      	S32 1      # chip-to-fpa tranformation order
+PSASTRO.CHIP.NY	     	S32 1      # chip-to-fpa tranformation order
+
+PSASTRO.CHIP.NSIGMA  	F32 3.0    # Nsigma clip for matched-fit 
+PSASTRO.CHIP.NITER   	S32 3      # number of clipping interations for matched-fit 
+
+PSASTRO.GRID.OFFSET  	F32 100.0  # maximum allowed offset for grid match
+PSASTRO.GRID.SCALE   	F32  50.0  # binning scale for grid match
+
+PSASTRO.GRID.MIN.ANGLE  F32   0.0  # starting angle
+PSASTRO.GRID.MAX.ANGLE  F32   0.0  # ending angle
+PSASTRO.GRID.DEL.ANGLE  F32   0.5  # steps between angle attempts
+
+PSASTRO.STARS.MAX       S32 300    # use no more than this number of sources
Index: /branches/eam_branch_20071023/psastro/psastro.pc.in
===================================================================
--- /branches/eam_branch_20071023/psastro/psastro.pc.in	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/psastro.pc.in	(revision 15358)
@@ -0,0 +1,12 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: libpsastro
+Description: Pan-STARRS Photometry Library
+Version: @VERSION@
+Requires: pslib psmodules
+Libs: -L${libdir} -lpsastro
+Libs.private: @PSASTRO_LIBS@
+Cflags: -I${includedir} @PSASTRO_CFLAGS@
Index: /branches/eam_branch_20071023/psastro/src/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psastro/src/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/.cvsignore	(revision 15358)
@@ -0,0 +1,14 @@
+*.o
+*.lo
+.libs
+.deps
+Makefile
+Makefile.in
+psastro
+psastro.loT
+psastroErrorCodes.h
+psastroErrorCodes.c
+libpsastro.la
+config.h
+config.h.in
+stamp-h1
Index: /branches/eam_branch_20071023/psastro/src/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psastro/src/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/Makefile.am	(revision 15358)
@@ -0,0 +1,68 @@
+
+lib_LTLIBRARIES = libpsastro.la
+libpsastro_la_CPPFLAGS = $(PSLIB_CFLAGS) $(PSMODULE_CFLAGS) $(PSASTRO_CFLAGS)
+
+bin_PROGRAMS = psastro
+psastro_CPPFLAGS = $(PSLIB_CFLAGS) $(PSMODULE_CFLAGS) $(PSASTRO_CFLAGS)
+psastro_LDFLAGS = $(PSLIB_LIBS) $(PSMODULE_LIBS) $(PSASTRO_LIBS)
+psastro_LDADD = libpsastro.la
+
+psastro_SOURCES = \
+	psastro.c		    \
+        psastroArguments.c	    \
+	psastroParseCamera.c   	    \
+	psastroDataLoad.c           \
+	psastroCleanup.c
+
+## move DataSave to psastro_SOURCES?
+
+libpsastro_la_SOURCES = \
+	psastroErrorCodes.c         \
+	psastroVersion.c            \
+	psastroDataSave.c           \
+	psastroDefineFiles.c        \
+	psastroAnalysis.c           \
+	psastroAstromGuess.c        \
+	psastroLoadRefstars.c       \
+	psastroChooseRefstars.c     \
+	psastroConvert.c	    \
+	psastroChipAstrom.c         \
+	psastroOneChip.c	    \
+	psastroUtils.c	       	    \
+	psastroTestFuncs.c          \
+	psastroLuminosityFunction.c \
+	psastroRefstarSubset.c      \
+	psastroMosaicAstrom.c       \
+	psastroMosaicGradients.c    \
+	psastroMosaicChipAstrom.c   \
+	psastroMosaicOneChip.c      \
+	psastroMosaicSetAstrom.c    \
+	psastroMosaicSetMatch.c     \
+	psastroDemoDump.c           \
+	psastroDemoPlot.c
+
+include_HEADERS = \
+	psastro.h \
+	psastroErrorCodes.h
+
+noinst_HEADERS = \
+	psastroInternal.h \
+	psastroStandAlone.h
+
+clean-local:
+	-rm -f TAGS
+
+# Tags for emacs
+tags:
+	etags `find . -name \*.[ch] -print`
+
+### Error codes.
+BUILT_SOURCES = psastroErrorCodes.h psastroErrorCodes.c
+CLEANFILES = psastroErrorCodes.h psastroErrorCodes.c
+EXTRA_DIST = psastroErrorCodes.dat psastroErrorCodes.h.in psastroErrorCodes.c.in
+
+psastroErrorCodes.h : psastroErrorCodes.dat psastroErrorCodes.h.in
+	$(ERRORCODES) --data=psastroErrorCodes.dat --outdir=. psastroErrorCodes.h
+
+psastroErrorCodes.c : psastroErrorCodes.dat psastroErrorCodes.c.in psastroErrorCodes.h
+	$(ERRORCODES) --data=psastroErrorCodes.dat --outdir=. psastroErrorCodes.c
Index: /branches/eam_branch_20071023/psastro/src/psastro.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastro.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastro.c	(revision 15358)
@@ -0,0 +1,53 @@
+# include "psastroStandAlone.h"
+
+static void usage (void) {
+    fprintf (stderr, "USAGE: psastro [-file image(s)] [-list imagelist] (output)\n");
+    exit (2);
+}
+
+int main (int argc, char **argv) {
+
+    pmConfig *config = NULL;
+
+    psTimerStart ("complete");
+
+    psastroErrorRegister();              // register our error codes/messages
+
+    // model inits are needed in pmSourceIO
+    // models defined in psphot/src/models are not available in psastro
+    pmModelClassInit ();
+
+    // load configuration information
+    config = psastroArguments (argc, argv);
+    if (!config) usage ();
+
+    // load identify the data sources
+    if (!psastroParseCamera (config)) {
+	psErrorStackPrint(stderr, "error setting up the camera\n");
+	exit (1);
+    }
+
+    // load the raw pixel data (from PSPHOT.SOURCES)
+    // select subset of stars for astrometry
+    if (!psastroDataLoad (config)) {
+	psErrorStackPrint(stderr, "error loading input data\n");
+	exit (1);
+    }
+
+    // run the full astrometry analysis (chip and/or mosaic)
+    if (!psastroAnalysis (config)) {
+	psErrorStackPrint(stderr, "failure in psastro analysis\n");
+	exit (1);
+    }
+    
+    // write out the results
+    if (!psastroDataSave (config)) {
+	psErrorStackPrint(stderr, "failed to write out data\n");
+	exit (1);
+    }
+
+    psLogMsg ("psastro", 3, "complete psastro run: %f sec\n", psTimerMark ("complete"));
+
+    psastroCleanup (config);
+    exit (EXIT_SUCCESS);
+}
Index: /branches/eam_branch_20071023/psastro/src/psastro.h
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastro.h	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastro.h	(revision 15358)
@@ -0,0 +1,80 @@
+/* This file defines the library functions available to external programs.  It must be included
+ * by programs which are compiled against psphot functions.
+ */
+
+# ifndef PSASTRO_H
+# define PSASTRO_H
+
+# include "psastroErrorCodes.h"
+# define PSASTRO_RECIPE "PSASTRO" // Name of the recipe to use
+
+# define psMemCopy(A)(psMemIncrRefCounter((A)))
+# define DEG_RAD 57.295779513082322
+# define RAD_DEG  0.017453292519943
+# define SIGN(X)  (((X) == 0) ? 0 : ((fabs((double)(X))) / (X)))
+
+// this structure represents a fit to the logN / logS curve for a set of stars
+// logN = offset + slope * logS
+typedef struct {
+    double mMin;			// minimum magnitude bin with data
+    double mMax;			// maximum magnitude bin with data
+    double offset;			// fitted line offset
+    double slope;			// fitted line slope
+    double mPeak;			// mag of peak bin 
+    int nPeak;				// # of stars in peak bin
+} pmLumFunc;
+
+bool              psastroDataSave (pmConfig *config);
+bool              psastroDefineFiles (pmConfig *config, pmFPAfile *input);
+bool              psastroAnalysis (pmConfig *config);
+
+bool              psastroConvertFPA (pmFPA *fpa, psMetadata *recipe);
+bool              psastroConvertReadout (pmReadout *readout, psMetadata *recipe);
+psArray          *pmSourceToAstromObj (psArray *sources);
+bool              psastroAstromGuess (int *nStars, pmConfig *config);
+
+psPlaneDistort   *psPlaneDistortIdentity ();
+bool              psPlaneDistortIsIdentity (psPlaneDistort *distort);
+
+psArray          *psastroLoadRefstars (pmConfig *config);
+bool              psastroChipAstrom (pmConfig *config);
+bool              psastroOneChip (pmFPA *fpa, pmChip *chip, psArray *refset, psArray *rawset, psMetadata *recipe, psMetadata *updates);
+bool              psastroChooseRefstars (pmConfig *config, psArray *refs);
+bool              psastroRefstarSubset (pmReadout *readout);
+pmLumFunc        *psastroLuminosityFunction (psArray *stars);
+
+// utility functions:
+bool              psastroUpdateChipToFPA (pmFPA *fpa, pmChip *chip, psArray *rawstars, psArray *refstars);
+bool              psastroWriteStars (char *filename, psArray *sources);
+bool              psastroWriteTransform (psPlaneTransform *map);
+
+// mosaic fitting functions
+bool 		  psastroMosaicGradients (pmFPA *fpa, psMetadata *recipe);
+bool 		  psastroMosaicCommonScale (pmFPA *fpa, psMetadata *recipe);
+bool 		  psastroMosaicAstrom (pmConfig *config);
+bool 		  psastroMosaicChipAstrom (pmFPA *fpa, psMetadata *recipe, int iteration);
+bool 		  psastroMosaicSetMatch (pmFPA *fpa, psMetadata *recipe, int iteration);
+bool 		  psastroMosaicSetAstrom (pmFPA *fpa);
+bool 		  psastroMosaicHeaders (pmConfig *config);
+bool 		  psastroMosaicRescaleChips (pmFPA *fpa);
+bool 		  psastroMosaicOneChip (pmChip *chip, pmReadout *readout, psMetadata *recipe, psMetadata *updates, int iteration);
+
+// Return version strings.
+psString 	  psastroVersion(void);
+psString 	  psastroVersionLong(void);
+
+// demo plots
+bool 		  psastroPlotRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe);
+bool 		  psastroPlotRefstars (psArray *refstars, psMetadata *recipe);
+bool 		  psastroPlotOneChipFit (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe);
+
+bool 		  psastroDumpRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip);
+bool 		  psastroDumpRefstars (psArray *refstars, char *filename);
+bool 		  psastroDumpMatches (pmFPA *fpa, char *filename);
+bool 		  psastroDumpStars (psArray *stars, char *filename);
+bool              psastroDumpMatchedStars (char *filename, psArray *rawstars, psArray *refstars, psArray *match);
+
+bool		  psastroMosaicSetAstrom_tmp (pmFPA *fpa);
+int 		  psastroSortByMag (const void *a, const void *b);
+
+# endif /* PSASTRO_H */
Index: /branches/eam_branch_20071023/psastro/src/psastroAnalysis.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroAnalysis.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroAnalysis.c	(revision 15358)
@@ -0,0 +1,73 @@
+# include "psastroInternal.h"
+
+bool psastroAnalysis (pmConfig *config) {
+
+    bool status;
+    int nStars;
+
+    // interpret the available initial astrometric information
+    // apply the initial guess
+    if (!psastroAstromGuess (&nStars, config)) {
+	psError (PSASTRO_ERR_UNKNOWN, false, "failed to determine initial astrometry guess\n");
+	return false;
+    }
+    if (nStars == 0) {
+	psLogMsg ("psastro", 2, "skipping astrometry analysis : no stars\n");
+	return true;
+    }
+
+    // XXX test retrun
+    // return true;
+
+    // load the reference stars overlapping the data stars
+    psArray *refs = psastroLoadRefstars(config);
+    if (!refs) {
+	psError (PSASTRO_ERR_UNKNOWN, false, "failed to load reference data\n");
+	return false;
+    }
+
+    if (!psastroChooseRefstars (config, refs)) {
+	psError (PSASTRO_ERR_UNKNOWN, false, "failed to select reference data for chips\n");
+	return false;
+    }
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+	return false;
+    }
+
+    // check the command-line arguments first
+    bool chipastro = psMetadataLookupBool (&status, config->arguments, "PSASTRO.CHIP.MODE");
+    if (!status) {
+	chipastro = psMetadataLookupBool (&status, recipe, "PSASTRO.CHIP.MODE");
+    }
+    bool mosastro  = psMetadataLookupBool (&status, config->arguments, "PSASTRO.MOSAIC.MODE");
+    if (!status) {
+	mosastro  = psMetadataLookupBool (&status, recipe, "PSASTRO.MOSAIC.MODE");
+    }
+    if (!chipastro && !mosastro) {
+	psLogMsg ("psastro", 3, "no astrometry mode selected, assuming chip astrometry\n");
+	chipastro = true;
+    }
+
+    if (chipastro) {
+      if (!psastroChipAstrom (config)) {
+	    psError (PSASTRO_ERR_UNKNOWN, false, "failed to perform single chip astrometry\n");
+	    return false;
+	}
+    } 
+    if (mosastro) {
+	if (!psastroMosaicAstrom (config)) {
+	    psError (PSASTRO_ERR_UNKNOWN, false, "failed to perform mosaic camera astrometry\n");
+	    return false;
+	}
+    }
+
+    // XXX how do we specify stack astrometry?
+    // psastroStackAstrom (config, refs);
+
+    psFree (refs);
+    return true;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroArguments.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroArguments.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroArguments.c	(revision 15358)
@@ -0,0 +1,78 @@
+# include "psastroStandAlone.h"
+
+pmConfig *psastroArguments (int argc, char **argv) {
+
+    bool status;
+    int N;
+
+    if (argc == 1) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "No arguments supplied");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    // load config data from default locations
+    pmConfig *config = pmConfigRead(&argc, argv, PSASTRO_RECIPE);
+    if (config == NULL) {
+        psError(PSASTRO_ERR_CONFIG, false, "Can't read site configuration");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    // save the following additional recipe values based on command-line options
+    // these options override the PSASTRO recipe values loaded from recipe files
+    psMetadata *options = pmConfigRecipeOptions (config, PSASTRO_RECIPE);
+
+    // photcode : used in output to supplement header data (argument or recipe?)
+    if ((N = psArgumentGet (argc, argv, "-photcode"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (options, PS_LIST_TAIL, "PHOTCODE", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // chip selection is used to limit chips to be processed
+    if ((N = psArgumentGet (argc, argv, "-chip"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "CHIP_SELECTIONS", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // apply mosastro mode?
+    if ((N = psArgumentGet (argc, argv, "-mosastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.MOSAIC.MODE", PS_META_REPLACE, "", false);
+    }
+    if ((N = psArgumentGet (argc, argv, "+mosastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.MOSAIC.MODE", PS_META_REPLACE, "", true);
+    }
+
+    // apply chipastro mode?
+    if ((N = psArgumentGet (argc, argv, "-chipastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.CHIP.MODE", PS_META_REPLACE, "", false);
+    }
+    if ((N = psArgumentGet (argc, argv, "+chipastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.CHIP.MODE", PS_META_REPLACE, "", true);
+    }
+
+    status = pmConfigFileSetsMD (config->arguments, &argc, argv, "INPUT", "-file", "-list");
+    if (!status) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Missing -file (input) or -list (input)");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    if (argc != 2) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Incorrect arguments supplied");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    // output positions is fixed
+    psMetadataAddStr (config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "", argv[1]);
+
+    psTrace("psastro", 1, "Done with psastroArguments...\n");
+    return (config);
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroAstromGuess.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroAstromGuess.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroAstromGuess.c	(revision 15358)
@@ -0,0 +1,161 @@
+# include "psastroInternal.h"
+
+// this function loads the header WCS astrometry terms into the fpa terms and applies the
+// astrometry to the detected objects.
+
+// this function assumes the initial astrometry arrives in the form of WCS keywords in the
+// headers corresponding to the chips.
+
+bool psastroAstromGuess (int *nStars, pmConfig *config) {
+
+    bool newFPA = true;
+    bool status = false;
+    double RAmin  = +FLT_MAX;
+    double RAmax  = -FLT_MAX;
+    double DECmin = +FLT_MAX;
+    double DECmax = -FLT_MAX;
+
+    double RAminSky = NAN;
+    double RAmaxSky = NAN;
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    *nStars = 0;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!");
+	return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+	return false;
+    }
+
+    // physical pixel scale in microns per pixel
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    if (!status) {
+	psError(PS_ERR_IO, true, "Failed to lookup pixel scale"); 
+	return false; 
+    } 
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPA *fpa = input->fpa;
+
+    // load mosaic-level astrometry?
+    bool bilevelAstrometry = false;
+    pmHDU *phu = pmFPAviewThisPHU (view, fpa);
+    if (phu) {
+      char *ctype = psMetadataLookupStr (NULL, phu->header, "CTYPE1");
+      if (ctype) {
+	bilevelAstrometry = !strcmp (&ctype[4], "-DIS");
+      }
+    }
+    if (bilevelAstrometry) {
+      pmAstromReadBilevelMosaic (fpa, phu->header);
+    } 
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists || !chip->data_exists) { continue; }
+
+        // read WCS data from the corresponding header
+        pmHDU *hdu = pmFPAviewThisHDU (view, fpa);
+	if (bilevelAstrometry) {
+	    if (!pmAstromReadBilevelChip (chip, hdu->header)) {
+		psWarning("Could not get WCS information from header for chip %d, skipping", view->chip); 
+		continue;
+	    } 
+	} else {
+	    if (!pmAstromReadWCS (fpa, chip, hdu->header, pixelScale)) {
+		psWarning("Could not get WCS information from header for chip %d, skipping", view->chip); 
+		continue;
+	    } 
+	}
+
+        if (newFPA) {
+            newFPA = false;
+	    while (fpa->toSky->R <        0) fpa->toSky->R += 2.0*M_PI;
+	    while (fpa->toSky->R > 2.0*M_PI) fpa->toSky->R -= 2.0*M_PI;
+            RAminSky = fpa->toSky->R - M_PI;
+            RAmaxSky = fpa->toSky->R + M_PI;
+        }
+
+        // apply the new WCS guess data to all of the data in the readouts
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+                if (rawstars == NULL) { continue; }
+
+		*nStars += rawstars->n;
+                for (int i = 0; i < rawstars->n; i++) {
+                    pmAstromObj *raw = rawstars->data[i];
+
+                    psPlaneTransformApply (raw->FP, chip->toFPA, raw->chip);
+                    psPlaneTransformApply (raw->TP, fpa->toTPA, raw->FP);
+                    psDeproject (raw->sky, raw->TP, fpa->toSky);
+
+                    // rationalize ra to sky range centered on boresite
+                    while (raw->sky->r < RAminSky) raw->sky->r += 2.0*M_PI;
+                    while (raw->sky->r > RAmaxSky) raw->sky->r -= 2.0*M_PI;
+
+                    RAmin = PS_MIN (raw->sky->r, RAmin);
+                    RAmax = PS_MAX (raw->sky->r, RAmax);
+
+                    DECmin = PS_MIN (raw->sky->d, DECmin);
+                    DECmax = PS_MAX (raw->sky->d, DECmax);
+                }
+
+		// dump or plot the resulting projected positions
+		if (psTraceGetLevel("psastro.dump") > 0) {
+		    psastroDumpRawstars (rawstars, fpa, chip);
+		}
+
+		if (psTraceGetLevel("psastro.plot") > 0) {
+		    psastroPlotRawstars (rawstars, fpa, chip, recipe);
+		}
+            }
+        }
+    }
+
+    // how many total sources are available to us?
+    psMetadataAddS32 (recipe, PS_LIST_TAIL, "NTOTSTAR",  PS_META_REPLACE, "", *nStars);
+    if (*nStars == 0) {
+	psLogMsg ("psastro", 2, "no sources available for astrometry\n");
+	psFree (view);
+	return true;
+    }
+
+    psLogMsg ("psastro", 2, "loaded raw data from %f,%f to %f,%f\n", 
+	      DEG_RAD*RAmin, DEG_RAD*DECmin, 
+	      DEG_RAD*RAmax, DEG_RAD*DECmax);
+
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "RA_MIN",  PS_META_REPLACE, "", RAmin);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "RA_MAX",  PS_META_REPLACE, "", RAmax);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DEC_MIN", PS_META_REPLACE, "", DECmin);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DEC_MAX", PS_META_REPLACE, "", DECmax);
+
+    psFree (view);
+    return true;
+}
+
+/* coordinate frame hierachy
+   pixels (on a given readout)
+   cell
+   chip
+   FP (focal plane)
+   TP (tangent plane)
+   sky (ra, dec)
+*/
Index: /branches/eam_branch_20071023/psastro/src/psastroChipAstrom.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroChipAstrom.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroChipAstrom.c	(revision 15358)
@@ -0,0 +1,98 @@
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+
+bool psastroChipAstrom (pmConfig *config) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+	return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+	return false;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPA *fpa = input->fpa;
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		// the absolute minimum number of stars is 4 (for order = 1)
+		if ((rawstars->n < 4) || (refstars->n < 4)) {
+		    readout->data_exists = false;
+		    psLogMsg ("psastro", 3, "insufficient rawstars (%ld) or refstars (%ld)", 
+			    rawstars->n, refstars->n);
+		    continue;
+		} 
+
+		char *filename = NULL;
+		char *chipname = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+
+		// save WCS and analysis metadata in update header
+		psMetadata *updates = psMetadataAlloc();
+
+		// XXX update the header with info to reflect the failure
+		if (!psastroOneChip (fpa, chip, refstars, rawstars, recipe, updates)) {
+		    readout->data_exists = false;
+		    psLogMsg ("psastro", 3, "failed to find a solution\n");
+		    psFree (updates);
+		    continue;
+		}
+		pmAstromWriteWCS (updates, fpa, chip, NONLIN_TOL);
+		psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_DATA_METADATA, "psastro header stats", updates);
+		psFree (updates);
+
+		if (psTraceGetLevel("psastro.dump") > 0) {
+		    psStringAppend (&filename, "rawstars.ch.%s.dat", chipname);
+		    psastroDumpStars (rawstars, filename);
+		    psFree (filename);
+		    filename = NULL;
+
+		    psStringAppend (&filename, "refstars.ch.%s.dat", chipname);
+		    psastroDumpStars (refstars, filename);
+		    psFree (filename);
+		    filename = NULL;
+		}
+	    }
+	}
+    }
+
+    psFree (view);
+    return true;
+}
+
+/* coordinate frame hierachy
+   pixels (on a given readout)
+   cell
+   chip
+   FP (focal plane)
+   TP (tangent plane)
+   sky (ra, dec)
+*/
Index: /branches/eam_branch_20071023/psastro/src/psastroChooseRefstars.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroChooseRefstars.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroChooseRefstars.c	(revision 15358)
@@ -0,0 +1,105 @@
+# include "psastroInternal.h"
+
+bool psastroChooseRefstars (pmConfig *config, psArray *refs) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+        return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data!\n");
+        return false;
+    }
+
+    float fieldExtra = psMetadataLookupS32 (&status, recipe, "PSASTRO.FIELD.EXTRA");
+    if (!status) fieldExtra = 0.0;
+
+    bool matchLumFunc = psMetadataLookupBool (&status, recipe, "PSASTRO.MATCH.LUMFUNC");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPA *fpa = input->fpa;
+
+    // sort by mag
+    psVector *index = psArraySortIndex (NULL, refs, psastroSortByMag);
+
+    int nMax = psMetadataLookupS32 (&status, recipe, "PSASTRO.MAX.NREF");
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            // XXX there can only be one readout per chip in astrometry, right?
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // read WCS data from the corresponding header
+                pmHDU *hdu = pmFPAviewThisHDU (view, fpa);
+
+                int Nx = psMetadataLookupS32 (&status, hdu->header, "NAXIS1");
+                int Ny = psMetadataLookupS32 (&status, hdu->header, "NAXIS2");
+
+                float minX = -fieldExtra*Nx;
+                float maxX = (1+fieldExtra)*Nx;
+                float minY = -fieldExtra*Ny;
+                float maxY = (1+fieldExtra)*Ny;
+
+                // the refstars is a subset within range of this chip
+                psArray *refstars = psArrayAllocEmpty (100);
+
+                // select the reference objects within range of this readout
+                // project the reference objects to this chip
+                for (int i = 0; i < refs->n; i++) {
+                    pmAstromObj *ref = pmAstromObjCopy(refs->data[index->data.S32[i]]);
+
+                    psProject (ref->TP, ref->sky, fpa->toSky);
+                    psPlaneTransformApply (ref->FP, fpa->fromTPA, ref->TP);
+                    psPlaneTransformApply (ref->chip, chip->fromFPA, ref->FP);
+
+                    // limit the X,Y range of the refs to the selected chip
+                    if (ref->chip->x < minX) goto skip;
+                    if (ref->chip->x > maxX) goto skip;
+                    if (ref->chip->y < minY) goto skip;
+                    if (ref->chip->y > maxY) goto skip;
+
+                    psArrayAdd (refstars, 100, ref);
+                skip:
+                    psFree (ref);
+
+		    if (nMax && (refstars->n >= nMax)) break;
+                }
+                psTrace ("psastro", 4, "Added %ld refstars\n", refstars->n);
+
+		psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.REFSTARS", PS_DATA_ARRAY, "astrometry matches", refstars);
+		psFree (refstars);
+
+		if (matchLumFunc) {
+		    // in this case, no PSASTRO.REFSTARS is added to readout->analysis
+		    if (!psastroRefstarSubset (readout)) {
+			psError(PSASTRO_ERR_DATA, false, "Can't determine an appropriate refstar subset\n");
+			return false;
+		    }
+		}
+            }
+        }
+    }
+
+    psFree (index);
+    psFree (view);
+    return true;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroCleanup.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroCleanup.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroCleanup.c	(revision 15358)
@@ -0,0 +1,17 @@
+# include "psastroStandAlone.h"
+
+void psastroCleanup (pmConfig *config) {
+
+    psFree (config);
+
+    psTimerStop ();
+    psMemCheckCorruption (stderr, true);
+    pmModelClassCleanup ();
+    psTimeFinalize ();
+    pmConceptsDone ();
+    pmConfigDone ();
+    fprintf (stderr, "found %d leaks at %s\n", psMemCheckLeaks (0, NULL, stdout, false), "psastro");
+    // fprintf (stderr, "found %d leaks at %s\n", psMemCheckLeaks (0, NULL, NULL, false), "psastro");
+
+    return;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroConvert.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroConvert.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroConvert.c	(revision 15358)
@@ -0,0 +1,124 @@
+# include "psastroInternal.h"
+// leak free 2006.04.27
+
+bool psastroConvertFPA (pmFPA *fpa, psMetadata *recipe) {
+
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                psastroConvertReadout (readout, recipe);
+            }
+        }
+    }
+    psFree (view);
+    return true;
+}
+
+// pass/apply the WCS information?
+bool psastroConvertReadout (pmReadout *readout, psMetadata *recipe) {
+
+    bool status;
+
+    // PSPHOT.SOURCES carries the pmSource objects (from psphot analysis or loaded externally)
+    psArray *sources = psMetadataLookupPtr (NULL, readout->analysis, "PSPHOT.SOURCES");
+    if (sources == NULL)
+        return false;
+
+    // convert the pmSource objects into pmAstromObj objects (drop !STAR and SATSTAR?)
+    psArray *inStars = pmSourceToAstromObj (sources);
+
+    // sort in ascending magnitude order
+    // psArraySort (inStars, psastroSortByMag);
+    // psVector *index = psArraySortIndex (sources, pmSourceSortBySN);
+    psVector *index = psArraySortIndex (NULL, inStars, psastroSortByMag);
+
+    // we are going to select the brighter Nmax subset for astrometry 
+    int nMax = psMetadataLookupS32 (&status, recipe, "PSASTRO.MAX.NRAW");
+    if (!status || !nMax) nMax = inStars->n; // 10 is really somewhat absurd as a lower limit
+
+    // choose the first nMax sources
+    int j = 0;
+    psArray *rawStars = psArrayAlloc (PS_MIN (nMax, inStars->n));
+    for (int i = 0; (i < inStars->n) && (j < rawStars->n); i++) {
+	int n = index->data.S32[i];
+	pmSource *source = sources->data[n];
+	if (source->mode & PM_SOURCE_MODE_SATSTAR) continue;
+	rawStars->data[j] = psMemIncrRefCounter (inStars->data[n]);
+	j++;
+    }
+    rawStars->n = j;
+
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.RAWSTARS", PS_DATA_ARRAY, "astrometry objects", rawStars);
+    psLogMsg ("psastro", 4, "loaded %ld sources, using %ld of %ld good sources\n", sources->n, rawStars->n, inStars->n);
+
+    psFree (index);
+    psFree (inStars);
+    psFree (rawStars);
+
+    return true;
+}
+
+// select a magnitude range?
+psArray *pmSourceToAstromObj (psArray *sources) {
+
+    psArray *objects = psArrayAllocEmpty (sources->n);
+
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+
+        // only accept the PSF sources?
+	// XXX drop SATSTAR?
+        if (source->type != PM_SOURCE_TYPE_STAR) continue;
+
+        pmModel *model = source->modelPSF;
+        if (model == NULL) continue;
+
+        psF32 *PAR = model->params->data.F32;
+
+        pmAstromObj *obj = pmAstromObjAlloc ();
+
+        // is the source magnitude calibrated in any sense?
+        obj->pix->x = PAR[2];
+        obj->pix->y = PAR[3];
+        obj->Mag = source->psfMag;
+
+        // XXX do we have the information giving the readout and cell offset?
+        // for the moment, assume chip == cell == readout
+        *obj->cell = *obj->pix;
+        *obj->chip = *obj->cell;
+
+        psArrayAdd (objects, 100, obj);
+        psFree (obj);
+    }
+    return objects;
+}
+
+// sort by Mag (ascending)
+int psastroSortByMag (const void *a, const void *b)
+{
+    const pmAstromObj *A = a;
+    const pmAstromObj *B = b;
+
+    psF32 mA = (isfinite(A->Mag)) ? A->Mag : FLT_MAX;
+    psF32 mB = (isfinite(B->Mag)) ? B->Mag : FLT_MAX;
+
+    psF32 diff = mA - mB;
+    if (diff > FLT_EPSILON) return (+1);
+    if (diff < FLT_EPSILON) return (-1);
+    return (0);
+}
+
Index: /branches/eam_branch_20071023/psastro/src/psastroDataLoad.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroDataLoad.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroDataLoad.c	(revision 15358)
@@ -0,0 +1,75 @@
+# include "psastroStandAlone.h"
+// this loop loads the data from the input files and selects the
+// brighter stars for astrometry
+// at the end of this function, the complete stellar data is loaded
+// into the correct fpa structure locations (readout.analysis:PSPHOT.SOURCES)
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroDataSave"); \
+  psFree (view); \
+  return false; \
+}
+  
+// all of the different astrometry analysis modes use the same data load loop
+bool psastroDataLoad (pmConfig *config) {
+
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+
+    psTimerStart ("psastro");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data!\n");
+	return false;
+    }
+
+    // de-activate all files except PSASTRO.INPUT
+    pmFPAfileActivate (config->files, false, NULL);
+    pmFPAfileActivate (config->files, true, "PSASTRO.INPUT");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // files associated with the science image
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    while ((chip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+	while ((cell = pmFPAviewNextCell (view, input->fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+	    // process each of the readouts
+	    while ((readout = pmFPAviewNextReadout (view, input->fpa, 1)) != NULL) {
+		if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+		if (!readout->data_exists) { continue; }
+
+		if (!psastroConvertReadout (readout, recipe)) ESCAPE;
+
+		if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+	    }
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+	}
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    psLogMsg ("psastro", 3, "load data : %f sec\n", psTimerMark ("psastro"));
+
+    psFree (view);
+    return true;
+}
+
Index: /branches/eam_branch_20071023/psastro/src/psastroDataSave.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroDataSave.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroDataSave.c	(revision 15358)
@@ -0,0 +1,69 @@
+# include "psastroInternal.h"
+// XXX leak free 2006.04.27
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroDataSave"); \
+  psFree (view); \
+  return false; \
+}
+  
+// this loop saves the photometry/astrometry data files
+bool psastroDataSave (pmConfig *config) {
+
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    // select the output data sources
+    pmFPAfile *output = psMetadataLookupPtr (NULL, config->files, "PSASTRO.OUTPUT");
+    if (!output) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find or interpret output file rule PSASTRO.OUTPUT!\n");
+	return false;
+    }
+
+    // de-activate all files except PSASTRO.OUTPUT
+    pmFPAfileActivate (config->files, false, NULL);
+    pmFPAfileActivate (config->files, true, "PSASTRO.OUTPUT");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // open/load files as needed
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    while ((chip = pmFPAviewNextChip (view, output->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+	while ((cell = pmFPAviewNextCell (view, output->fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+	    // process each of the readouts
+	    while ((readout = pmFPAviewNextReadout (view, output->fpa, 1)) != NULL) {
+		if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+		if (!readout->data_exists) { continue; }
+
+		if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+	    }
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+	}
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    // activate all files except PSASTRO.OUTPUT
+    if (!pmFPAfileActivate (config->files, true, NULL)) ESCAPE;
+    if (!pmFPAfileActivate (config->files, false, "PSASTRO.OUTPUT")) ESCAPE;
+
+    psFree (view);
+    return true;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroDefineFiles.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroDefineFiles.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroDefineFiles.c	(revision 15358)
@@ -0,0 +1,26 @@
+# include "psastroInternal.h"
+
+bool psastroDefineFiles (pmConfig *config, pmFPAfile *input) {
+
+    // these calls bind the I/O handle to the specified fpa
+    pmFPAfile *output = NULL;
+
+    output = pmFPAfileDefineOutput (config, input->fpa, "PSASTRO.OUTPUT");
+    if (!output) {
+	psError(PSASTRO_ERR_CONFIG, false, "Failed to build FPA from PSASTRO.INPUT");
+	return false;
+    }
+    output->save = true;
+
+# if (0)
+    // optionally save output plots
+    if (!pmFPAfileDefineOutput (config, input->fpa, "SOURCE.PLOT.MOMENTS")) {
+	psTrace ("psphot", 3, "Cannot find a rule for SOURCE.PLOT.MOMENTS");
+    }
+    if (!pmFPAfileDefineOutput (config, input->fpa, "SOURCE.PLOT.PSFMODEL")) {
+	psTrace ("psphot", 3, "Cannot find a rule for SOURCE.PLOT.PSFMODEL");
+    }
+# endif
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroDemoDump.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroDemoDump.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroDemoDump.c	(revision 15358)
@@ -0,0 +1,174 @@
+# include "psastroInternal.h"
+
+// this function is used for test purposes (-trace psastro.dump.psastroAstromGuess 1)
+bool psastroDumpStars (psArray *stars, char *filename) {
+
+    FILE *f = fopen (filename, "w");
+
+    for (int i = 0; i < stars->n; i++) {
+	pmAstromObj *obj = stars->data[i];
+
+	// write out the upward projections
+	fprintf (f, "%d  %f %f  %f  %f %f  %f %f  %f %f\n", i,
+		 obj->sky->r, obj->sky->d, obj->Mag, 
+		 obj->TP->x, obj->TP->y, 
+		 obj->FP->x, obj->FP->y, 
+		 obj->chip->x, obj->chip->y);
+    }
+    fclose (f);
+    return true;
+}
+
+// this function is used for test purposes (-trace psastro.dump.psastroAstromGuess 1)
+bool psastroDumpRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip) {
+
+    char *filename = NULL;
+    char *chipname = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+
+    psStringAppend (&filename, "rawstars.up.%s.dat", chipname);
+    FILE *f1 = fopen (filename, "w");
+    psFree (filename);
+    filename = NULL;
+
+    psStringAppend (&filename, "rawstars.dn.%s.dat", chipname);
+    FILE *f2 = fopen (filename, "w");
+    psFree (filename);
+    filename = NULL;
+
+    for (int i = 0; i < rawstars->n; i++) {
+	pmAstromObj *raw = rawstars->data[i];
+
+	psPlane *fp = psPlaneAlloc();
+	psPlane *tp = psPlaneAlloc();
+	psPlane *ch = psPlaneAlloc();
+			
+	psProject (tp, raw->sky, fpa->toSky);
+	psPlaneTransformApply (fp, fpa->fromTPA, tp);
+	psPlaneTransformApply (ch, chip->fromFPA, fp);
+			
+	// write out the upward projections
+	fprintf (f1, "%d  %f %f  %f  %f %f  %f %f  %f %f\n", i,
+		 raw->sky->r, raw->sky->d, raw->Mag, 
+		 raw->TP->x, raw->TP->y, 
+		 raw->FP->x, raw->FP->y, 
+		 raw->chip->x, raw->chip->y);
+		
+	// write out the downward projections
+	fprintf (f2, "%d  %f %f  %f  %f %f  %f %f  %f %f\n", i,
+		 raw->sky->r, raw->sky->d, raw->Mag, 
+		 tp->x, tp->y, 
+		 fp->x, fp->y, 
+		 ch->x, ch->y);
+		
+	psFree (fp);
+	psFree (tp);
+	psFree (ch);
+    }
+
+    fclose (f1);
+    fclose (f2);
+    return true;
+}
+
+bool psastroDumpMatchedStars (char *filename, psArray *rawstars, psArray *refstars, psArray *match) {
+    
+    FILE *f = fopen (filename, "w");
+
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+	fprintf (f, "%f %f  %f %f  %f %f  %f %f  %f   |   ",  
+		 DEG_RAD*raw->sky->r, DEG_RAD*raw->sky->d, 
+		 raw->TP->x, raw->TP->y, 
+		 raw->FP->x, raw->FP->y, 
+		 raw->chip->x, raw->chip->y, raw->Mag);
+
+	fprintf (f, "%f %f  %f %f  %f %f  %f %f  %f\n", 
+		 DEG_RAD*ref->sky->r, DEG_RAD*ref->sky->d, 
+		 ref->TP->x, ref->TP->y, 
+		 ref->FP->x, ref->FP->y, 
+		 ref->chip->x, ref->chip->y, ref->Mag);
+    }
+    fclose (f);
+
+    return true;
+}
+
+// this function is used for test purposes (-trace psastro.dump.psastroLoadRefstars 1)
+bool psastroDumpRefstars (psArray *refstars, char *filename) {
+
+    FILE *f = fopen (filename, "w");
+
+    for (int i = 0; i < refstars->n; i++) {
+	pmAstromObj *ref = refstars->data[i];
+
+	// write out the refstar data
+	fprintf (f, "%d  %f %f  %f\n", i,
+		 ref->sky->r, ref->sky->d, ref->Mag);
+    }
+
+    fclose (f);
+    return true;
+}
+
+bool psastroDumpMatches (pmFPA *fpa, char *filename) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    FILE *f = fopen (filename, "w");
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) continue;
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) continue;
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) continue;
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) continue;
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) continue;
+		psTrace ("psastro", 4, "Trying %ld refstars\n", refstars->n);
+
+		psArray *matches = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (matches == NULL) continue;
+
+		for (int i = 0; i < matches->n; i++) {
+		    pmAstromMatch *match = matches->data[i];
+
+		    pmAstromObj *raw = rawstars->data[match->raw];
+		    fprintf (f, "%f %f  %f %f  %f %f  %f %f  %f   |   ",  
+			     DEG_RAD*raw->sky->r, DEG_RAD*raw->sky->d, 
+			     raw->TP->x, raw->TP->y, 
+			     raw->FP->x, raw->FP->y, 
+			     raw->chip->x, raw->chip->y, raw->Mag);
+
+		    pmAstromObj *ref = refstars->data[match->ref];
+		    fprintf (f, "%f %f  %f %f  %f %f  %f %f  %f\n", 
+			     DEG_RAD*ref->sky->r, DEG_RAD*ref->sky->d, 
+			     ref->TP->x, ref->TP->y, 
+			     ref->FP->x, ref->FP->y, 
+			     ref->chip->x, ref->chip->y, ref->Mag);
+		}
+	    }
+	}
+    }
+    fclose (f);
+    psFree (view);
+    return true;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroDemoPlot.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroDemoPlot.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroDemoPlot.c	(revision 15358)
@@ -0,0 +1,492 @@
+# include "psastroInternal.h"
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+
+bool pmKapaPlotVectorTriple_AutoLimitsZscale_OpenGraph (int kapa, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing);
+
+bool psastroPlotRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe)
+{
+    Graphdata graphdata;
+    KapaSection section;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    bool status = false;
+    float iMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MIN");
+    float iMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MAX");
+
+    KapaResize (kapa, 1000, 1000);
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    section.dx = 0.5;
+    section.dy = 0.5;
+
+    psVector *xVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+
+    section.x = 0.0;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a0");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    int n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+	pmAstromObj *raw = rawstars->data[i];
+	if (!isfinite(raw->Mag)) continue;
+	if (raw->Mag < iMagMin) continue;
+	if (raw->Mag > iMagMax) continue;
+
+	xVec->data.F32[n] = raw->chip->x;
+	yVec->data.F32[n] = raw->chip->y;
+	zVec->data.F32[n] = raw->Mag;
+	n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    section.x = 0.5;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+	pmAstromObj *raw = rawstars->data[i];
+	if (!isfinite(raw->Mag)) continue;
+	if (raw->Mag < iMagMin) continue;
+	if (raw->Mag > iMagMax) continue;
+
+	xVec->data.F32[n] = raw->FP->x;
+	yVec->data.F32[n] = raw->FP->y;
+	zVec->data.F32[n] = raw->Mag;
+	n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    section.x = 0.0;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a2");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+	pmAstromObj *raw = rawstars->data[i];
+	if (!isfinite(raw->Mag)) continue;
+	if (raw->Mag < iMagMin) continue;
+	if (raw->Mag > iMagMax) continue;
+
+	xVec->data.F32[n] = raw->TP->x;
+	yVec->data.F32[n] = raw->TP->y;
+	zVec->data.F32[n] = raw->Mag;
+	n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    section.x = 0.5;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a3");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+	pmAstromObj *raw = rawstars->data[i];
+	if (!isfinite(raw->Mag)) continue;
+	if (raw->Mag < iMagMin) continue;
+	if (raw->Mag > iMagMax) continue;
+
+	xVec->data.F32[n] = DEG_RAD*raw->sky->r;
+	yVec->data.F32[n] = DEG_RAD*raw->sky->d;
+	zVec->data.F32[n] = raw->Mag;
+	n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // pause and wait for user input: 
+    // continue, save (provide name), ??
+    char key[10], name[80];
+    fprintf (stdout, "(s)ave plot or [c]ontinue? ");
+    fgets (key, 8, stdin);
+    if (key[0] == 's') {
+	fprintf (stdout, "enter plot name [rawstars.png]: ");
+	fscanf (stdin, "%s", name);
+	if (!strcmp (name, "")) strcpy (name, "rawstars.png");
+	KapaPNG (kapa, name);
+    }
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+bool psastroPlotRefstars (psArray *refstars, psMetadata *recipe)
+{
+    Graphdata graphdata;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    bool status = false;
+    float rMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MIN");
+    float rMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MAX");
+
+    KapaResize (kapa, 1000, 1000);
+    KapaInitGraph (&graphdata);
+    KapaClearSections (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    psVector *xVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+
+    int n = 0;
+    for (int i = 0; i < refstars->n; i++) {
+	pmAstromObj *ref = refstars->data[i];
+	if (!isfinite(ref->Mag)) continue;
+	if (ref->Mag > rMagMax) continue;
+	if (ref->Mag < rMagMin) continue;
+
+	xVec->data.F32[n] = DEG_RAD*ref->sky->r;
+	yVec->data.F32[n] = DEG_RAD*ref->sky->d;
+	zVec->data.F32[n] = ref->Mag;
+	n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // pause and wait for user input: 
+    // continue, save (provide name), ??
+    char key[10], name[80];
+    fprintf (stdout, "(s)ave plot or [c]ontinue? ");
+    fgets (key, 8, stdin);
+    if (key[0] == 's') {
+	fprintf (stdout, "enter plot name [refstars.png]: ");
+	fscanf (stdin, "%s", name);
+	if (!strcmp (name, "")) strcpy (name, "refstars.png");
+	KapaPNG (kapa, name);
+    }
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+bool psastroPlotOneChipFit (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe) {
+
+    Graphdata graphdata;
+    KapaSection section;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    bool status = false;
+    float iMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MIN");
+    float iMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MAX");
+    float rMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MIN");
+    float rMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MAX");
+
+    KapaResize (kapa, 1000, 1000);
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    section.dx = 0.5;
+    section.dy = 0.5;
+
+    psVector *xVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (match->n, PS_TYPE_F32);
+
+    // X vs dX
+    section.x = 0.0;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a0");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    int n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+	
+	if (raw->Mag < iMagMin) continue;
+	if (raw->Mag > iMagMax) continue;
+	if (ref->Mag < rMagMin) continue;
+	if (ref->Mag > rMagMax) continue;
+
+	xVec->data.F32[n] = raw->chip->x;
+	yVec->data.F32[n] = raw->chip->x - ref->chip->x;
+	zVec->data.F32[n] = raw->Mag;
+	n++;
+    }	
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // X vs dY
+    section.x = 0.5;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+	
+	if (!isfinite(raw->Mag)) continue;
+	if (raw->Mag < iMagMin) continue;
+	if (raw->Mag > iMagMax) continue;
+	if (ref->Mag < rMagMin) continue;
+	if (ref->Mag > rMagMax) continue;
+
+	xVec->data.F32[n] = raw->chip->x;
+	yVec->data.F32[n] = raw->chip->y - ref->chip->y;
+	zVec->data.F32[n] = raw->Mag;
+	n++;
+    }	
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // Y vs dX
+    section.x = 0.0;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a2");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+	
+	if (!isfinite(raw->Mag)) continue;
+	if (raw->Mag < iMagMin) continue;
+	if (raw->Mag > iMagMax) continue;
+	if (ref->Mag < rMagMin) continue;
+	if (ref->Mag > rMagMax) continue;
+
+	xVec->data.F32[n] = raw->chip->y;
+	yVec->data.F32[n] = raw->chip->x - ref->chip->x;
+	zVec->data.F32[n] = raw->Mag;
+	n++;
+    }	
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // Y vs dY
+    section.x = 0.5;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a3");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+	
+	if (!isfinite(raw->Mag)) continue;
+	if (raw->Mag < iMagMin) continue;
+	if (raw->Mag > iMagMax) continue;
+	if (ref->Mag < rMagMin) continue;
+	if (ref->Mag > rMagMax) continue;
+
+	xVec->data.F32[n] = raw->chip->y;
+	yVec->data.F32[n] = raw->chip->y - ref->chip->y;
+	zVec->data.F32[n] = raw->Mag;
+	n++;
+    }	
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // *** X vs Y plot (different window)
+    int kapa2 = KapaOpenNamedSocket ("kapa", "XvsY");
+    if (kapa2 == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    KapaResize (kapa2, 1000, 1000);
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa2);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 2;
+    graphdata.style = 2;
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+
+    xVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    yVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    zVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+
+    // X vs Y by mag (raw)
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+	if (!isfinite(raw->Mag)) continue;
+	if (raw->Mag < iMagMin) continue;
+	if (raw->Mag > iMagMax) continue;
+
+	xVec->data.F32[n] = raw->chip->x;
+	yVec->data.F32[n] = raw->chip->y;
+	zVec->data.F32[n] = raw->Mag;
+	n++;
+    }	
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 7;
+    graphdata.style = 2;
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+
+    xVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    yVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    zVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+
+    // X vs Y by mag (raw)
+    n = 0;
+    for (int i = 0; i < refstars->n; i++) {
+        pmAstromObj *ref = refstars->data[i];
+	if (!isfinite(ref->Mag)) continue;
+	if (ref->Mag < rMagMin) continue;
+	if (ref->Mag > rMagMax) continue;
+
+	xVec->data.F32[n] = ref->chip->x;
+	yVec->data.F32[n] = ref->chip->y;
+	zVec->data.F32[n] = ref->Mag;
+	n++;
+    }	
+    xVec->n = yVec->n = zVec->n = n;
+    pmKapaPlotVectorTriple_AutoLimitsZscale_OpenGraph (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    // pause and wait for user input: 
+    // continue, save (provide name), ??
+    char key[10], name[80];
+    fprintf (stdout, "(s)ave plot or [c]ontinue? ");
+    fgets (key, 8, stdin);
+    if (key[0] == 's') {
+	fprintf (stdout, "enter plot name [chipfit.png]: ");
+	fscanf (stdin, "%s", name);
+	if (!strcmp (name, "")) strcpy (name, "chipfit.png");
+	KapaPNG (kapa, name);
+    }
+
+    close (kapa2);
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+bool pmKapaPlotVectorTriple_AutoLimitsZscale_OpenGraph (int kapa, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing)
+{
+
+    // set limits based on data values
+    float zmin = +FLT_MAX;
+    float zmax = -FLT_MAX;
+    for (int i = 0; i < xVec->n; i++) {
+        zmin = PS_MIN (zmin, zVec->data.F32[i]);
+        zmax = PS_MAX (zmax, zVec->data.F32[i]);
+    }
+
+    // set the scale vector
+    psVector *zScale = psVectorAlloc (zVec->n, PS_DATA_F32);
+
+    float range = zmax - zmin;
+    if (range == 0.0) {
+        psVectorInit (zScale, 1.0);
+    } else {
+        for (int i = 0; i < zVec->n; i++) {
+            if (increasing) {
+                zScale->data.F32[i] = PS_MIN (1.5, PS_MAX(0.05, 1.5*(zVec->data.F32[i] - zmin)/range));
+            } else {
+                zScale->data.F32[i] = PS_MIN (1.5, PS_MAX(0.05, 1.5*(zmax - zVec->data.F32[i])/range));
+            }
+        }
+    }
+
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, graphdata);
+
+    // the point size will be scaled from the z vector
+    graphdata->size = -1;
+    KapaPrepPlot (kapa, xVec->n, graphdata);
+    KapaPlotVector (kapa, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa, yVec->n, yVec->data.F32, "y");
+    KapaPlotVector (kapa, zVec->n, zScale->data.F32, "z");
+    psFree (zScale);
+    return true;
+}
+
+# else
+
+bool psastroPlotRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip)
+{
+    return false;
+}
+
+bool psastroPlotRefstars (psArray *refstars)
+{
+    return false;
+}
+
+bool psastroPlotOneChipFit (psArray *rawstars, psArray *refstars, psArray *match, pmAstromFitResults *results)
+{
+    return false;
+}
+
+# endif
Index: /branches/eam_branch_20071023/psastro/src/psastroEnforceChip.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroEnforceChip.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroEnforceChip.c	(revision 15358)
@@ -0,0 +1,75 @@
+# include "psastroInternal.h"
+
+bool psastroEnforceChips (pmFPA *fpa, psMetadata *recipe) {
+
+  pmChip *chip = NULL;
+  // identify reference astrometry table, stored as an fpa? better format?
+
+  pmFPAfile *refAstrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.REF.ASTROM");
+
+  pmFPAview *view = pmFPAviewAlloc (0);
+
+  // physical pixel scale in microns per pixel
+  double pixelTol = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.TOLERANCE");
+  if (!status) {
+    psError(PS_ERR_IO, true, "Failed to lookup pixel tolerance"); 
+    return false; 
+  } 
+  double angleTol = psMetadataLookupF32 (&status, recipe, "PSASTRO.ANGLE.TOLERANCE");
+  if (!status) {
+    psError(PS_ERR_IO, true, "Failed to lookup angle tolerance"); 
+    return false; 
+  } 
+
+  // loop over all chips
+  while ((obsChip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+    psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, obsChip->file_exists, obsChip->process);
+    if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+    // set the chip astrometry using the refAstrom file
+    pmChip *refChip = pmFPAviewThisChip (view, refAstrom->fpa);
+
+    // bad Astrometry test:  ref pixel or angle outside nominal
+
+    psPlane refPixel = {0.0, 0.0, 0.0, 0.0};
+    psPlane obsCoord, refCoord;
+
+    // find location of 0,0 pixel in focal plane coords for this chip
+    psPlaneTransformApply (&obsCoord, obsChip->toFPA, &refPixel);
+
+    // find location of 0,0 pixel in focal plane coords for ref chip
+    psPlaneTransformApply (&refCoord, refChip->toFPA, &refPixel);
+    
+    psPlane offPixel = {0.0, 0.0, 0.0, 0.0};
+    psPlane obsOffPt, refOffPt;
+
+    // find location of 0,0 pixel in focal plane coords for this chip
+    psPlaneTransformApply (&obsOffPt, obsChip->toFPA, &offPixel);
+
+    // find location of 0,0 pixel in focal plane coords for ref chip
+    psPlaneTransformApply (&refOffPt, refChip->toFPA, &offPixel);
+    
+    double obsAngle = atan2 (obsOffPt.y - obsCoord.y, obsOffPt.x - obsCoord.x);
+
+    badAstrom = false;
+    badAstrom |= fabs(obsCoord.x - refCoord.x) > pixelTol;
+    badAstrom |= fabs(obsCoord.y - refCoord.y) > pixelTol;
+    badAstrom |= fabs(obsAngle   - refAngle)   > angleTol;
+
+    if (!badAstrom) continue;
+
+    psFree (obsChip->toFPA);
+    psFree (obsChip->fromFPA);
+
+    // XXX can we do a straight copy?  I think so, but we may need to apply a scaling factor based
+    // on the toSky or toTPA values
+    obsChip->toFPA   = psMemIncrRefCounter (refChip->toFPA);
+    obsChip->fromFPA = psMemIncrRefCounter (refChip->fromFPA);
+  }
+  return true;
+}
+
+// XXX for this function to work, I need to:
+// 1 *) define badAstrom (ref pixel too far from nominal?) (ref angle too far off?)
+// 2) load the refAstrom fpa in the pmFPAfileIO stage 
+// 3) allow for this operation to be optional
Index: /branches/eam_branch_20071023/psastro/src/psastroErrorCodes.c.in
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroErrorCodes.c.in	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroErrorCodes.c.in	(revision 15358)
@@ -0,0 +1,25 @@
+/*
+ * The line
+    { PSASTRO_ERR_$X{ErrorCode}, "$X{ErrorDescription}"},
+ * (without the Xs)
+ * will be replaced by values from errorCodes.dat
+ */
+#include "pslib.h"
+#include "psastroErrorCodes.h"
+
+void psastroErrorRegister(void)
+{
+    static psErrorDescription errors[] = {
+       { PSASTRO_ERR_BASE, "First value we use; lower values belong to psLib" },
+       { PSASTRO_ERR_${ErrorCode}, "${ErrorDescription}"},
+    };
+    static int nerror = PSASTRO_ERR_NERROR - PSASTRO_ERR_BASE; // number of values in enum
+
+    for (int i = 0; i < nerror; i++) {
+       psErrorDescription *tmp = psAlloc(sizeof(psErrorDescription));
+       *tmp = errors[i];
+       psErrorRegister(tmp, 1);
+       psFree(tmp);			/* it's on the internal list */
+    }
+    nerror = 0;			                // don't register more than once
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroErrorCodes.dat
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroErrorCodes.dat	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroErrorCodes.dat	(revision 15358)
@@ -0,0 +1,12 @@
+#
+# This file is used to generate pmErrorClasses.h
+#
+BASE = 300		First value we use; lower values belong to psLib
+UNKNOWN			Unknown PM error code
+NOT_IMPLEMENTED		Desired feature is not yet implemented
+ARGUMENTS		Incorrect arguments
+CONFIG			Problem in configure files
+IO			Problem in FITS I/O
+WCS      		Error interpreting FITS WCS information
+DATA                    Problem in data values
+REFSTARS                Problem in data values
Index: /branches/eam_branch_20071023/psastro/src/psastroErrorCodes.h.in
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroErrorCodes.h.in	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroErrorCodes.h.in	(revision 15358)
@@ -0,0 +1,18 @@
+#if !defined(PSASTRO_ERROR_CODES_H)
+#define PSASTRO_ERROR_CODES_H
+/*
+ * The line
+ *  PSASTRO_ERR_$X{ErrorCode},
+ * (without the X)
+ *
+ * will be replaced by values from errorCodes.dat
+ */
+typedef enum {
+    PSASTRO_ERR_BASE = 600,
+    PSASTRO_ERR_${ErrorCode},
+    PSASTRO_ERR_NERROR
+} psastroErrorCode;
+
+void psastroErrorRegister(void);
+
+#endif
Index: /branches/eam_branch_20071023/psastro/src/psastroInternal.h
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroInternal.h	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroInternal.h	(revision 15358)
@@ -0,0 +1,16 @@
+# ifdef HAVE_CONFIG_H
+# include <config.h>
+# endif
+
+# ifndef PSASTRO_INTERNAL_H
+# define PSASTRO_INTERNAL_H
+
+# include <stdio.h>
+# include <string.h>
+# include <strings.h>  // for strcasecmp
+# include <unistd.h>   // for unlink
+# include <pslib.h>
+# include <psmodules.h>
+# include "psastro.h"
+
+#endif
Index: /branches/eam_branch_20071023/psastro/src/psastroLoadRefstars.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroLoadRefstars.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroLoadRefstars.c	(revision 15358)
@@ -0,0 +1,122 @@
+# include "psastroInternal.h"
+# define ELIXIR_MODE 1
+
+psArray *psastroLoadRefstars (pmConfig *config) {
+
+    int fd;
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+
+    // DVO APIs expect decimal degrees
+    float RAmin  = DEG_RAD*psMetadataLookupF32(NULL, recipe, "RA_MIN");
+    float RAmax  = DEG_RAD*psMetadataLookupF32(NULL, recipe, "RA_MAX");
+    float DECmin = DEG_RAD*psMetadataLookupF32(NULL, recipe, "DEC_MIN");
+    float DECmax = DEG_RAD*psMetadataLookupF32(NULL, recipe, "DEC_MAX");
+    // float MAGmax = psMetadataLookupF32(NULL, recipe, "MAG_MAX");
+
+    // XXX CATDIR needs to look up abstracted name from psastro.config
+    // XXX which is then selected from the top-level ipprc.config
+    char *catdir = psMetadataLookupStr(NULL, recipe, "DVO.CATDIR");
+    psString CATDIR = pmConfigConvertFilename(catdir, config, false); // Resolved filename
+    PS_ASSERT (CATDIR, NULL);
+
+    char *getstarCommand = psMemIncrRefCounter(psMetadataLookupStr(NULL, recipe, "DVO.GETSTAR"));
+    PS_ASSERT (getstarCommand, NULL);
+
+    // issue the following command:
+    // getstar -region RAmin RAmax DECmin DECmax
+    char tempFile[64];
+    sprintf (tempFile, "/tmp/psastro.XXXXXX");
+    if ((fd = mkstemp (tempFile)) == -1) {
+        psError(PSASTRO_ERR_REFSTARS, true, "error creating temp output file\n");
+        psFree(CATDIR);
+        return NULL;
+    }
+    close (fd);
+
+    psTimerStart ("psastro");
+
+    // use fork to add timeout capability
+    if (ELIXIR_MODE) {
+        psStringAppend (&getstarCommand, " -D CATFORMAT elixir");
+    } else {
+        psStringAppend (&getstarCommand, " -D CATFORMAT panstarrs");
+    }
+
+    // check for default name (use .ptolemyrc), or use specified CATDIR
+    if (strcasecmp(CATDIR, "NONE")) {
+        psStringAppend (&getstarCommand, " -D CATDIR %s", CATDIR);
+    }
+    psFree(CATDIR);
+
+    // psStringAppend (&getstarCommand, " -D CATMODE mef -maglim %f -region %f %f %f %f -o %s", MAGmax, RAmin, DECmin, RAmax, DECmax, tempFile);
+    // XXX TEST : no magnitude limit
+    // XXX need to specify the desired photometry system
+    psStringAppend (&getstarCommand, " -D CATMODE mef -region %f %f %f %f -o %s", RAmin, DECmin, RAmax, DECmax, tempFile);
+    psTrace ("psastro", 3, "%s\n", getstarCommand);
+
+    // XXX use psPipe: catch stderr, stdout, allow for Nsec timeout...
+    status = system (getstarCommand);
+    if (status) {
+        psError(PSASTRO_ERR_REFSTARS, true, "error loading reference data\n");
+        return NULL;
+    }
+    psFree (getstarCommand);
+
+    psLogMsg ("psastro", 3, "ran getstar : %f sec\n", psTimerMark ("psastro"));
+
+    // the output from getstar is a file with the Average table
+    psFits *fits = psFitsOpen (tempFile, "r");
+
+    if (ELIXIR_MODE) {
+        psFitsMoveExtName (fits, "DVO_AVERAGE_ELIXIR");
+    } else {
+        psFitsMoveExtName (fits, "DVO_AVERAGE_PANSTARRS");
+    }
+
+    psTimerStart ("psastro");
+    long numSources = psFitsTableSize(fits); // Number of sources in table
+
+    // convert the Average table to the pmAstromObj entries
+    psArray *refstars = psArrayAllocEmpty (numSources);
+    for (int i = 0; i < numSources; i++) {
+        pmAstromObj *ref = pmAstromObjAlloc ();
+
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+
+        // DVO tables are stored in degrees
+        if (ELIXIR_MODE) {
+            ref->sky->r   = RAD_DEG*psMetadataLookupF32 (&status, row, "RA");
+            ref->sky->d   = RAD_DEG*psMetadataLookupF32 (&status, row, "DEC");
+            ref->Mag      = 0.001*psMetadataLookupS32 (&status, row, "MAG");  // ELIXIR uses millimags
+        } else {
+            ref->sky->r   = RAD_DEG*psMetadataLookupF64 (&status, row, "RA");
+            ref->sky->d   = RAD_DEG*psMetadataLookupF64 (&status, row, "DEC");
+            ref->Mag      = psMetadataLookupF32 (&status, row, "MAG"); // PANSTARRS uses mags
+        }
+
+        psArrayAdd (refstars, 100, ref);
+        psFree (ref);
+        psFree (row);
+    }
+    psLogMsg ("psastro", 3, "loaded %ld reference stars : %f sec\n", refstars->n, psTimerMark ("psastro"));
+
+    psTrace ("psastro", 3, "loaded %ld reference stars from (%10.6f,%10.6f) - (%10.6f,%10.6f)\n",
+             refstars->n, RAmin, DECmin, RAmax, DECmax);
+
+    psFitsClose (fits);
+    unlink (tempFile);
+
+    // dump or plot the available refstars
+    if (psTraceGetLevel("psastro.dump") > 0) {
+        psastroDumpRefstars (refstars, "refstars.dat");
+    }
+
+    if (psTraceGetLevel("psastro.plot") > 0) {
+        psastroPlotRefstars (refstars, recipe);
+    }
+
+    return refstars;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroLuminosityFunction.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroLuminosityFunction.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroLuminosityFunction.c	(revision 15358)
@@ -0,0 +1,135 @@
+# include "psastroInternal.h"
+# define dMag 0.1
+// XXX put this in config?
+
+static void pmLumFuncFree (pmLumFunc *func) {
+
+  if (func == NULL) return;
+
+  return;
+}
+
+pmLumFunc *pmLumFuncAlloc () {
+
+  pmLumFunc *func = (pmLumFunc *) psAlloc(sizeof(pmLumFunc));
+  psMemSetDeallocator(func, (psFreeFunc) pmLumFuncFree);
+
+  func->mMin = 0.0;
+  func->mMax = 0.0;
+  func->slope = 0.0;
+  func->offset = 0.0;
+
+  func->mPeak = 0.0;
+  func->nPeak = 0;
+
+  return func;
+}
+
+pmLumFunc *psastroLuminosityFunction (psArray *stars) {
+
+  // determine the max and min magnitude for the array of stars
+  pmAstromObj *star = stars->data[0];
+  double mMin = star->Mag;
+  double mMax = star->Mag;
+  for (int i = 0; i < stars->n; i++) {
+    star = stars->data[i];
+    mMin = PS_MIN (star->Mag, mMin);
+    mMax = PS_MAX (star->Mag, mMax);
+  }
+
+  int nBin = 1 + (mMax - mMin) / dMag;
+  if (nBin <= 1) {
+    psLogMsg ("psastro", 4, "insufficient valid stars for this readout\n");
+    return NULL;
+  }
+
+  // create a histogram of the magnitudes
+  // bin[0] = mMin : mMin + dMag
+  // bin[i] = mMin + i*dMag : mMin + (i+1)*dMag
+  psVector *nMags = psVectorAlloc (nBin, PS_TYPE_F32);
+  psVectorInit (nMags, 0);
+
+  for (int i = 0; i < stars->n; i++) {
+    star = stars->data[i];
+    if (!isfinite(star->Mag)) continue;
+    int bin = (star->Mag - mMin) / dMag;
+    if (bin < 0) psAbort("bin cannot be negative!");
+    if (bin >= nBin) psAbort("bin cannot be > %d!", nBin);
+    nMags->data.F32[bin] += 1.0;
+  }
+
+  // find the peak and position
+  int iPeak = 0;
+  int nPeak = 0;
+  for (int i = 0; i < nMags->n; i++) {
+    if (nMags->data.F32[i] < nPeak) continue;
+    iPeak = i;
+    nPeak = nMags->data.F32[i];
+  }
+
+  psVector *lnMag = psVectorAllocEmpty (nBin, PS_TYPE_F32);
+  psVector *Mag = psVectorAllocEmpty (nBin, PS_TYPE_F32);
+
+  // create 2 vectors represnting the dlogN/dlogS line
+  // exclude bins with no stars
+  // exclude points after the peak with N < 0.8*peak
+  int n = 0;
+  for (int i = 0; i < nMags->n; i++) {
+    if (nMags->data.F32[i] < 1) continue;
+    if ((i > iPeak) && (nMags->data.F32[i] < 0.8*nPeak)) continue;
+    lnMag->data.F32[n] = log10 (nMags->data.F32[i]);
+    Mag->data.F32[n] = mMin + (i + 0.5)*dMag;
+    n++;
+  }
+  lnMag->n = n;
+  Mag->n = n;
+  psLogMsg ("psastro", 4, "fitting %d points to luminosity function\n", n);
+
+  psVector *mask = psVectorAlloc (Mag->n, PS_TYPE_MASK);
+  psVectorInit (mask, 0);
+
+  psPolynomial1D *line = psPolynomial1DAlloc (PS_POLYNOMIAL_ORD, 1);
+  psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+  stats->clipSigma = 3.0;
+  stats->clipIter = 3;
+
+  if (!psVectorClipFitPolynomial1D(line, stats, mask, 0xff, lnMag, NULL, Mag)) {
+      psLogMsg ("psastro", 4, "Failed to fit the luminosity function\n");
+      return(NULL);
+  }
+
+  // find min and max unmasked magnitudes
+  double mMinValid = NAN;
+  double mMaxValid = NAN;
+  for (int i = 0; i < Mag->n; i++) {
+    if (mask->data.U8[i]) continue;
+    if (isnan(mMinValid) || (Mag->data.F32[i] < mMinValid)) {
+      mMinValid = Mag->data.F32[i];
+    }
+    if (isnan(mMaxValid) || (Mag->data.F32[i] > mMaxValid)) {
+      mMaxValid = Mag->data.F32[i];
+    }
+  }
+
+  psLogMsg ("psastro", 4, "logN = %f + %f mag\n", line->coeff[0], line->coeff[1]);
+  psLogMsg ("psastro", 4, "logN vs mag residuals: %f +/- %f\n", stats->sampleMedian, stats->sampleStdev);
+
+  pmLumFunc *lumFunc = pmLumFuncAlloc ();
+  lumFunc->mMin = mMinValid;
+  lumFunc->mMax = mMaxValid;
+  lumFunc->offset = line->coeff[0];
+  lumFunc->slope = line->coeff[1];
+  lumFunc->mPeak = mMin + (iPeak + 0.5)*dMag;
+  lumFunc->nPeak = nPeak;
+
+  psFree (lnMag);
+  psFree (nMags);
+  psFree (Mag);
+  psFree (mask);
+  psFree (line);
+  psFree (stats);
+
+  return (lumFunc);
+}
+
+
Index: /branches/eam_branch_20071023/psastro/src/psastroMosaicAstrom.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroMosaicAstrom.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroMosaicAstrom.c	(revision 15358)
@@ -0,0 +1,139 @@
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+
+// XXX require this fpa to have multiple chip extensions and a PHU?
+bool psastroMosaicAstrom (pmConfig *config) {
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, false, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, false, "Can't find input data!\n");
+	return false;
+    }
+
+    pmFPA *fpa = input->fpa;
+
+    // XXX before we do object matches, we need to fix failed chips
+    // compare chips with supplied mosaic model
+    // adjust significant outliers to match model
+
+    // given the existing per-chip astrometry, determine matches between raw and ref stars
+    // is this needed? yes, if we didn't do SingleChip astrometry first
+    if (!psastroMosaicSetMatch (fpa, recipe, 0)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to match raw and ref stars for mosaic");
+	return false;
+    }
+    if (psTraceGetLevel("psastro.dump") > 0) { psastroDumpMatches (fpa, "match.0.dat"); }
+
+    // fitted chips will follow the local plate-scale, hiding the distortion
+    // modify the chip->toFPA scaling to match knowledge about pixel scale,
+    // then recalculate raw and ref positions
+    if (!psastroMosaicCommonScale (fpa, recipe)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to set a common scale for the chips");
+	return false;
+    }
+    if (psTraceGetLevel("psastro.dump") > 0) { psastroDumpMatches (fpa, "match.1.dat"); }
+
+    // fit the distortion by fitting its gradient
+    // apply the new distortion terms up and down
+    // refit the per-chip terms with linear fits only
+    if (!psastroMosaicGradients (fpa, recipe)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure mosaic gradients");
+	return false;
+    }
+    if (psTraceGetLevel("psastro.dump") > 0) { psastroDumpMatches (fpa, "match.2.dat"); }
+
+    // measure the astrometry for the chips under the distortion term
+    if (!psastroMosaicChipAstrom (fpa, recipe, 0)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure chip astrometry in mosaic mode");
+	return false;
+    }
+    if (psTraceGetLevel("psastro.dump") > 0) { psastroDumpMatches (fpa, "match.3.dat"); }
+
+    // do a second pass on the distortion with improved chip positions and rotations
+    // first, re-perform the match with a slightly tighter circle
+    if (!psastroMosaicSetMatch (fpa, recipe, 1)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to match raw and ref stars for mosaic (2nd pass)");
+	return false;
+    }
+    if (!psastroMosaicCommonScale (fpa, recipe)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to set a common scale for the chips (2nd pass)");
+	return false;
+    }
+    if (!psastroMosaicGradients (fpa, recipe)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure mosaic gradients (2nd pass)");
+	return false;
+    }
+    if (psTraceGetLevel("psastro.dump") > 0) { psastroDumpMatches (fpa, "match.4.dat"); }
+
+    if (!psastroMosaicChipAstrom (fpa, recipe, 1)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure chip astrometry in mosaic mode (2nd pass)");
+	return false;
+    }
+    if (psTraceGetLevel("psastro.dump") > 0) { psastroDumpMatches (fpa, "match.5.dat"); }
+
+    // do a third pass on the distortion with improved chip positions and rotations
+    // first, re-perform the match with a slightly tighter circle
+    if (!psastroMosaicSetMatch (fpa, recipe, 2)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to match raw and ref stars for mosaic (3rd pass)");
+	return false;
+    }
+    if (!psastroMosaicCommonScale (fpa, recipe)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to set a common scale for the chips (3rd pass)");
+	return false;
+    }
+    if (!psastroMosaicGradients (fpa, recipe)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure mosaic gradients (3rd pass)");
+	return false;
+    }
+    if (psTraceGetLevel("psastro.dump") > 0) { psastroDumpMatches (fpa, "match.6.dat"); }
+
+    if (!psastroMosaicChipAstrom (fpa, recipe, 2)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure chip astrometry in mosaic mode (3rd pass)");
+	return false;
+    }
+    if (psTraceGetLevel("psastro.dump") > 0) { psastroDumpMatches (fpa, "match.7.dat"); }
+
+    // now fit the chips under the common distortion with higher-order terms
+    // first, re-perform the match with a slightly tighter circle
+    if (!psastroMosaicSetMatch (fpa, recipe, 2)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to match raw and ref stars for mosaic (3rd pass)");
+	return false;
+    }
+    if (!psastroMosaicChipAstrom (fpa, recipe, 3)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure chip astrometry in mosaic mode (3rd pass)");
+	return false;
+    }
+    if (psTraceGetLevel("psastro.dump") > 0) { psastroDumpMatches (fpa, "match.8.dat"); }
+
+    // save WCS and analysis metadata in update header.
+    psMetadata *updates = psMetadataAlloc();
+    if (!pmAstromWriteBilevelMosaic (updates, fpa, NONLIN_TOL)) {
+	psAbort ("failed to save header terms");
+    }
+
+    psMetadataAddMetadata (fpa->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+    psFree (updates);
+
+    // update the headers based on the results
+    // XXX need to add global summary statistics
+    // psastroMosaicHeaders (config);
+
+    return true;
+}
+
+/* coordinate frame hierachy
+ * pixels (on a given readout)
+ * cell
+ * chip
+ * FP (focal plane)
+ * TP (tangent plane)
+ * sky (ra, dec)
+ */
Index: /branches/eam_branch_20071023/psastro/src/psastroMosaicChipAstrom.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroMosaicChipAstrom.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroMosaicChipAstrom.c	(revision 15358)
@@ -0,0 +1,44 @@
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+
+bool psastroMosaicChipAstrom (pmFPA *fpa, psMetadata *recipe, int iteration) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// save WCS and analysis metadata in update header
+		psMetadata *updates = psMetadataAlloc();
+
+		if (!psastroMosaicOneChip (chip, readout, recipe, updates, iteration)) {
+		    readout->data_exists = false;
+		    psLogMsg ("psastro", 3, "failed to find a solution for %d,%d,%d\n", view->chip, view->cell, view->readout);
+		    psFree (updates);
+		    continue;
+		}
+
+		// create the header keywords to descripe the results
+		pmAstromWriteBilevelChip (updates, chip, NONLIN_TOL);
+		psMetadataAddMetadata (readout->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+		psFree (updates);
+	    }
+	}
+    }
+    psFree (view);
+    return true;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroMosaicGetGrads.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroMosaicGetGrads.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroMosaicGetGrads.c	(revision 15358)
@@ -0,0 +1,47 @@
+# include "psastroInternal.h"
+
+psArray *psastroMosaicGetGrads (pmFPA *fpa, psMetadata *recipe) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    psArray *grads = NULL;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	
+	psRegion *region = pmChipExtent (chip);
+
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		psArray *match = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (match == NULL) { continue; }
+
+		// measure the local gradients for this set of stars
+		// the new elements are added to the incoming gradient structure 
+		grads = pmAstromMeasureGradients (grads, rawstars, refstars, match, region, 2, 2);
+	    }
+	}
+	psFree (region);
+    }
+    return (grads);
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroMosaicGradients.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroMosaicGradients.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroMosaicGradients.c	(revision 15358)
@@ -0,0 +1,94 @@
+# include "psastroInternal.h"
+
+bool psastroMosaicGradients (pmFPA *fpa, psMetadata *recipe) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    psArray *gradients = NULL;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    int nXcell = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.GRADIENT.NX");
+    int nYcell = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.GRADIENT.NY");
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->toFPA) { continue; }
+	
+	psRegion *region = pmChipExtent (chip);
+
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		psArray *match = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (match == NULL) { continue; }
+
+		// measure the local gradients for this set of stars
+		// XXX 2,2 are the number of test boxes on the chip.  put this in the recipe
+		gradients = pmAstromMeasureGradients (gradients, rawstars, refstars, match, region, nXcell, nYcell);
+	    }
+	}
+	psFree (region);
+    }
+
+    // allocate mosaic-level polynomial transformation and set masks needed by DVO
+    int order = psMetadataLookupF32 (&status, recipe, "PSASTRO.MOSAIC.ORDER");
+    if (!status) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to find single-chip fit order\n");
+        return false;
+    }
+    psFree (fpa->toTPA);
+    fpa->toTPA = psPlaneTransformAlloc (order, order);
+    for (int i = 0; i <= fpa->toTPA->x->nX; i++) {
+        for (int j = 0; j <= fpa->toTPA->x->nY; j++) {
+            if (i + j > order) {
+		fpa->toTPA->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+		fpa->toTPA->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+            }
+        }
+    }
+
+    // physical pixel scale in microns per pixel (FP is in physical units, chip is in pixels)
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    if (!status) {
+	psError(PS_ERR_IO, false, "Failed to lookup pixel scale"); 
+	return false; 
+    } 
+
+    // fit the measured gradients with the telescope distortion model (polynomial order based on toTPA)
+    if (!pmAstromFitDistortion (fpa, gradients, pixelScale)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to fit the distortion terms\n");
+	psFree (gradients);
+	psFree (view);
+        return false;
+    }
+	
+    if (!psastroMosaicSetAstrom (fpa)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to apply mosaic distortion terms\n");
+	psFree (gradients);
+	psFree (view);
+        return false;
+    }
+
+    psFree (gradients);
+    psFree (view);
+    return true;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroMosaicHeaders.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroMosaicHeaders.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroMosaicHeaders.c	(revision 15358)
@@ -0,0 +1,59 @@
+# include "psastroInternal.h"
+
+bool psastroMosaicHeaders (pmConfig *config) {
+
+    bool status = false;
+    pmChip *chip = NULL;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+ 	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data!\n");
+	return false;
+    }
+
+    char *mosastro = psMetadataLookupStr (NULL, config->arguments, "MOSASTRO");
+
+    double plateScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLATE.SCALE");
+    if (!status) plateScale = 1.0;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPA *fpa = input->fpa;
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+
+        // read WCS data from the corresponding header
+        pmHDU *hdu = pmFPAviewThisHDU (view, fpa);
+
+        pmAstromWriteBilevelChip (chip->toFPA, hdu->header, plateScale);
+    }
+
+    psMetadata *mosaic = pmAstromWriteBilevelMosaic (fpa->toSky, fpa->toTPA, plateScale);
+
+    // XXX what is the EXTNAME??
+    psFits *fits = psFitsOpen (mosastro, "w");
+    psFitsWriteBlank(fits, mosaic, "");
+    psFitsClose (fits);
+
+    psFree (view);
+    return true;
+}
+
+/* coordinate frame hierachy
+   pixels (on a given readout)
+   cell
+   chip
+   FP (focal plane)
+   TP (tangent plane)
+   sky (ra, dec)
+*/
Index: /branches/eam_branch_20071023/psastro/src/psastroMosaicOneChip.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroMosaicOneChip.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroMosaicOneChip.c	(revision 15358)
@@ -0,0 +1,144 @@
+# include "psastroInternal.h"
+
+# define REQUIRED_RECIPE_VALUE(VALUE, NAME, TYPE, MESSAGE)\
+  VALUE = psMetadataLookup##TYPE (&status, recipe, NAME); \
+  if (!status) { \
+   psError(PSASTRO_ERR_CONFIG, false, MESSAGE); \
+   return false; } 
+
+bool psastroMosaicOneChip (pmChip *chip, pmReadout *readout, psMetadata *recipe, psMetadata *updates, int iteration) {
+
+    bool status;
+    char errorWord[64];
+    char orderWord[64];
+
+    assert (iteration < 4);
+
+    PS_ASSERT_PTR_NON_NULL(chip,    false);
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(recipe,  false);
+    PS_ASSERT_PTR_NON_NULL(updates, false);
+
+    // select the raw objects for this readout
+    psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+    if (rawstars == NULL) return false;
+
+    psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+    if (refstars == NULL) return false;
+
+    psArray *match = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+    if (match == NULL) return false;
+
+    // correct radius to FP units (physical pixel scale in microns per pixel)
+    REQUIRED_RECIPE_VALUE (double pixelScale, "PSASTRO.PIXEL.SCALE", F32, "Failed to lookup pixel scale"); 
+
+    // allowed limits for valid solutions
+    snprintf (errorWord, 64, "PSASTRO.MOSAIC.MAX.ERROR.N%d", iteration);
+    REQUIRED_RECIPE_VALUE (float maxError, errorWord, F32, "failed to find single-chip max allowed error\n");
+    REQUIRED_RECIPE_VALUE (int minNstar, "PSASTRO.MOSAIC.MIN.NSTAR", S32, "failed to find single-chip min allowed stars\n");
+
+    // set the order of the per-chip fit (higher order only if iteration > 0)
+    REQUIRED_RECIPE_VALUE (int defaultOrder, "PSASTRO.MOSAIC.CHIP.ORDER", S32, "failed to find mosaic chip-level fit default order\n");
+
+    snprintf (orderWord, 64, "PSASTRO.MOSAIC.CHIP.ORDER.N%d", iteration);
+    int order = psMetadataLookupS32 (&status, recipe, orderWord);
+    if (!status || (order == -1)) {
+	order = defaultOrder;
+    }
+
+    // modify the order to correspond to the actual number of matched stars:
+    if ((match->n < 15) && (order >= 3)) order = 2;
+    if ((match->n < 11) && (order >= 2)) order = 1;
+    if ((match->n <  8) && (order >= 1)) order = 0;
+    if ((match->n <  3) || (order < 0) || (order > 3)) {
+	psLogMsg ("psastro", 3, "insufficient stars (%ld) or invalid order (%d)", match->n, order); 
+	return false; 
+    } 
+
+    // create output toFPA; set masks appropriate to the Elixir DVO astrometry format if we are
+    // fitting 0th order, use the current polynomial of whatever order, with higher order
+    // coefficients frozen to the current values
+    if (order == 0) {
+	// set FIT mask for all higher order terms of the existing solution
+	// any existing SET masks will be retained.
+	for (int i = 0; i <= chip->toFPA->x->nX; i++) {
+	    for (int j = 0; j <= chip->toFPA->x->nY; j++) {
+		if (i + j > 0) {
+		    chip->toFPA->x->coeffMask[i][j] |= PS_POLY_MASK_FIT;
+		    chip->toFPA->y->coeffMask[i][j] |= PS_POLY_MASK_FIT;
+		}
+	    }
+	}
+    } else {
+	psFree (chip->toFPA);
+	chip->toFPA = psPlaneTransformAlloc (order, order);
+	for (int i = 0; i <= chip->toFPA->x->nX; i++) {
+	    for (int j = 0; j <= chip->toFPA->x->nY; j++) {
+		if (i + j > order) {
+		    chip->toFPA->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+		    chip->toFPA->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+		}
+	    }
+	}
+    }
+
+    // XXX allow statitic to be set by the user
+    psStats *fitStats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+    fitStats->clipSigma = psMetadataLookupF32 (&status, recipe, "PSASTRO.MOSAIC.CHIP.NSIGMA");
+    fitStats->clipIter = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.CHIP.NITER");
+
+    // need to pass in an update header, sent in from above
+    pmAstromFitResults *results = pmAstromMatchFit (chip->toFPA, rawstars, refstars, match, fitStats);
+    if (!results) {
+	psError(PSASTRO_ERR_DATA, false, "failed to perform the matched fit\n");
+	return false;
+    }
+
+    // toSky converts from FPA & TPA units (microns) to sky units (radians)
+    pmFPA *fpa = chip->parent;
+    float plateScale = 0.5*(fpa->toSky->Xs + fpa->toSky->Ys)*3600.0*PM_DEG_RAD;
+
+    // pixError is the average 1D scatter in pixels ('results' are in FPA units = microns)
+    float pixError = 0.5*(results->xStats->clippedStdev + results->yStats->clippedStdev) / pixelScale;
+
+    // astError is the average 1D scatter in arcsec ('results' are in FPA units = microns)
+    float astError = 0.5*(results->xStats->clippedStdev + results->yStats->clippedStdev) * plateScale;
+    int astNstar = results->yStats->clippedNvalues;
+
+    // if we clip away too many stars, the order may be invalid
+    if (order == 3) { minNstar = PS_MAX (15, minNstar); }
+    if (order == 2) { minNstar = PS_MAX (11, minNstar); }
+    if (order == 1) { minNstar = PS_MAX ( 8, minNstar); }
+
+    bool validSolution = true;
+
+    // XXX should these result in errors or be handled another way?
+    psLogMsg ("psastro", PS_LOG_INFO, "astrometry solution: error: %f arcsec, Nstars: %d", astError, astNstar);
+    if (astError > maxError) {
+	psLogMsg("psastro", PS_LOG_INFO, "residual error is too large, failed to find a solution: %f > %f", astError, maxError);
+	validSolution = false;
+    }
+    if (astNstar < minNstar) {
+	psLogMsg("psastro", PS_LOG_INFO, "solution uses too few stars: %d < %d", astNstar, minNstar);
+	validSolution = false;
+    }
+
+    // DVO expects NASTRO = 0 if we fail to find a solution
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "PERROR",   PS_META_REPLACE, "astrometry error (pixels)", pixError);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "CERROR",   PS_META_REPLACE, "astrometry error (arcsec)", astError);
+    if (validSolution) {
+	psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", astError/sqrt(astNstar));
+	psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", astNstar);
+    } else {
+	psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", 0.0);
+	psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", 0);
+    }
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "EQUINOX",  PS_META_REPLACE, "", 2000.0); // XXX this is bogus: should be defined based on equinox of refstars
+
+    // determine fromFPA transformation and apply new transformation to raw & ref stars
+    psastroUpdateChipToFPA (fpa, chip, rawstars, refstars);
+
+    psFree (fitStats);
+    psFree (results);
+    return validSolution;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroMosaicSetAstrom.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroMosaicSetAstrom.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroMosaicSetAstrom.c	(revision 15358)
@@ -0,0 +1,51 @@
+# include "psastroInternal.h"
+
+bool psastroMosaicSetAstrom (pmFPA *fpa) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		for (int i = 0; i < rawstars->n; i++) {
+		    pmAstromObj *raw = rawstars->data[i];
+	
+		    psPlaneTransformApply (raw->FP, chip->toFPA, raw->chip);
+		    psPlaneTransformApply (raw->TP, fpa->toTPA, raw->FP);
+		    psDeproject (raw->sky, raw->TP, fpa->toSky);
+		}
+
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		for (int i = 0; i < refstars->n; i++) {
+		    pmAstromObj *ref = refstars->data[i];
+	
+		    psProject (ref->TP, ref->sky, fpa->toSky);
+		    psPlaneTransformApply (ref->FP, fpa->fromTPA, ref->TP);
+		    psPlaneTransformApply (ref->chip, chip->fromFPA, ref->FP);
+		}
+	    }
+	}
+    }
+    psFree (view);
+    return true;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroMosaicSetMatch.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroMosaicSetMatch.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroMosaicSetMatch.c	(revision 15358)
@@ -0,0 +1,54 @@
+# include "psastroInternal.h"
+
+bool psastroMosaicSetMatch (pmFPA *fpa, psMetadata *recipe, int iteration) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+    char radiusWord[64];
+
+    // use small radius to match stars (assume starting astrometry is good)
+    bool status = false; 
+    sprintf (radiusWord, "PSASTRO.MOSAIC.RADIUS.N%d", iteration);
+    double RADIUS = psMetadataLookupF32 (&status, recipe, radiusWord); 
+    if (!status) { 
+	psError(PS_ERR_IO, false, "Failed to lookup matching radius: %s", radiusWord); 
+	return NULL; 
+    } 
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+		psTrace ("psastro", 4, "Trying %ld refstars\n", refstars->n);
+
+		psArray *matches = pmAstromRadiusMatchChip (rawstars, refstars, RADIUS);
+		psTrace ("psastro", 4, "Matched %ld refstars\n", matches->n);
+
+		// XXX drop the old one
+		psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.MATCH", PS_DATA_ARRAY | PS_META_REPLACE, "astrometry matches", matches);
+		psFree (matches);
+	    }
+	}
+    }
+    psFree (view);
+    return true;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroOneChip.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroOneChip.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroOneChip.c	(revision 15358)
@@ -0,0 +1,212 @@
+# include "psastroInternal.h"
+
+# define REQUIRED_RECIPE_VALUE(VALUE, NAME, TYPE)\
+  VALUE = psMetadataLookup##TYPE (&status, recipe, NAME); \
+  if (!status) { \
+   psAbort ("Failed to find %s in recipe", NAME); }
+
+bool psastroOneChip (pmFPA *fpa, pmChip *chip, psArray *refstars, psArray *rawstars, psMetadata *recipe, psMetadata *updates) {
+
+    bool status;
+    pmAstromStats *stats = NULL;
+
+    // default value for match/fit : radius is in pixels
+    REQUIRED_RECIPE_VALUE (double RADIUS, "PSASTRO.MATCH.RADIUS", F32); 
+
+    // run the match/fit sequence NITER times
+    REQUIRED_RECIPE_VALUE (int nIter, "PSASTRO.MATCH.FIT.NITER", S32); 
+
+    // correct radius to FP units (physical pixel scale in microns per pixel)
+    REQUIRED_RECIPE_VALUE (double pixelScale, "PSASTRO.PIXEL.SCALE", F32); 
+    RADIUS *= pixelScale;
+
+    // select the desired chip order
+    REQUIRED_RECIPE_VALUE (int order, "PSASTRO.CHIP.ORDER", S32);
+
+    // allowed limits for valid solutions
+    REQUIRED_RECIPE_VALUE (float maxError, "PSASTRO.MAX.ERROR", F32);
+    REQUIRED_RECIPE_VALUE (int minNstar, "PSASTRO.MIN.NSTAR", S32);
+
+    // do we need to get a rough initial match?
+    REQUIRED_RECIPE_VALUE (bool gridSearch, "PSASTRO.GRID.SEARCH", Bool);
+
+    // do we need to get a rough initial match?
+    REQUIRED_RECIPE_VALUE (int maxNstar, "PSASTRO.GRID.NSTAR.MAX", S32);
+
+    if (gridSearch) {
+	// generate the bright subset of maxNstar entries (note: rawstars is sorted by S/N)
+	psArray *gridStars = psArrayAlloc (PS_MIN (maxNstar, rawstars->n));
+	for (int i = 0; (i < maxNstar) && (i < rawstars->n); i++) {
+	    gridStars->data[i] = psMemIncrRefCounter (rawstars->data[i]);
+	}
+
+	// find initial offset / rotation
+	pmAstromStats *gridStats = pmAstromGridMatch (gridStars, refstars, recipe);
+	if (gridStats == NULL) {
+	    psLogMsg ("psastro", 3, "failed to find a grid match solution\n");
+	    psFree (gridStars);
+	    return false;
+	}
+	psLogMsg ("psastro", 3, "basic grid search result - offset: %f,%f pixels, rotation: %f deg\n", gridStats->offset.x, gridStats->offset.y, DEG_RAD*gridStats->angle);
+
+	// tweak the position by finding peak of matches stars
+	stats = pmAstromGridTweak (gridStars, refstars, recipe, gridStats);
+	if (stats == NULL) {
+	    psLogMsg ("psastro", 3, "failed to measure tweaked grid solution\n");
+	    psFree (gridStats);
+	    psFree (gridStars);
+	    return false;
+	}
+	psLogMsg ("psastro", 3, "tweak grid search result - offset: %f,%f pixels, rotation: %f deg\n", stats->offset.x, stats->offset.y, DEG_RAD*stats->angle);
+
+	// adjust the chip.toFPA terms only
+	pmAstromGridApply (chip->toFPA, stats);
+	psastroUpdateChipToFPA (fpa, chip, rawstars, refstars);
+	psFree (gridStats);
+	psFree (gridStars);
+    }
+
+    psArray *match = NULL;
+    psStats *fitStats = NULL;
+    pmAstromFitResults *results = NULL;
+
+    for (int iter = 0; iter < nIter; iter++) {
+	
+	char name[128];
+
+	sprintf (name, "PSASTRO.MATCH.RADIUS.N%d", iter);
+	float radius = psMetadataLookupF32 (&status, recipe, name);
+	radius *= pixelScale;
+	if (!status || (radius == 0.0)) {
+	    radius = RADIUS;
+	}
+
+
+	// use small radius to match stars
+	match = pmAstromRadiusMatchFP (rawstars, refstars, radius);
+	if (match == NULL) {
+	    psLogMsg ("psastro", 3, "failed to find radius-matched sources\n");
+	    psFree (stats);
+	    return false;
+	}
+
+	// modify the order to correspond to the actual number of matched stars:
+	if ((match->n < 11) && (order >= 3)) order = 2;
+	if ((match->n <  7) && (order >= 2)) order = 1;
+	if ((match->n <  4) && (order >= 1)) order = 0;
+	if (order < 1) {
+	    psLogMsg ("psastro", 3, "insufficient stars or invalid order: %ld stars", match->n); 
+	    psFree (match);
+	    psFree (stats);
+	    return false; 
+	} 
+
+	// create output toFPA; set masks appropriate to the Elixir DVO astrometry format
+	psFree (chip->toFPA);
+	chip->toFPA = psPlaneTransformAlloc (order, order);
+	for (int i = 0; i <= chip->toFPA->x->nX; i++) {
+	    for (int j = 0; j <= chip->toFPA->x->nY; j++) {
+		if (i + j > order) {
+		    chip->toFPA->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+		    chip->toFPA->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+		}
+	    }
+	}
+
+	// XXX allow statitic to be set by the user
+	// fitStats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+	fitStats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+	fitStats->clipSigma = psMetadataLookupF32 (&status, recipe, "PSASTRO.CHIP.NSIGMA");
+	fitStats->clipIter = psMetadataLookupS32 (&status, recipe, "PSASTRO.CHIP.NITER");
+
+	// improved fit for astrometric terms
+	results = pmAstromMatchFit (chip->toFPA, rawstars, refstars, match, fitStats);
+	if (!results) {
+	    psLogMsg ("psastro", 3, "failed to perform the matched fit\n");
+	    psFree (match);
+	    psFree (stats);
+	    psFree (fitStats);
+	    return false;
+	}
+    
+	// determine fromFPA transformation and apply new transformation to raw & ref stars
+	psastroUpdateChipToFPA (fpa, chip, rawstars, refstars);
+    
+	if (iter < nIter - 1) {
+	    psFree (fitStats);
+	    psFree (results);
+	    psFree (match);
+	}
+
+	// toSky converts from FPA & TPA units (microns) to sky units (radians)
+	float plateScale = 0.5*(fpa->toSky->Xs + fpa->toSky->Ys)*3600.0*PM_DEG_RAD;
+	// float astError = 0.5*(results->xStats->clippedStdev + results->yStats->clippedStdev) * plateScale;
+	float astError = 0.5*(results->xStats->robustStdev + results->yStats->robustStdev) * plateScale;
+	int astNstar = results->yStats->clippedNvalues;
+	psLogMsg ("psastro", PS_LOG_INFO, "pass %d, error: %f arcsec, Nstars: %d", iter, astError, astNstar);
+    }
+
+
+
+    // toSky converts from FPA & TPA units (microns) to sky units (radians)
+    float plateScale = 0.5*(fpa->toSky->Xs + fpa->toSky->Ys)*3600.0*PM_DEG_RAD;
+
+    // pixError is the average 1D scatter in pixels ('results' are in FPA units = microns)
+    // float pixError = 0.5*(results->xStats->clippedStdev + results->yStats->clippedStdev) / pixelScale;
+    float pixError = 0.5*(results->xStats->robustStdev + results->yStats->robustStdev) / pixelScale;
+
+    // astError is the average 1D scatter in arcsec ('results' are in FPA units = microns)
+    // float astError = 0.5*(results->xStats->clippedStdev + results->yStats->clippedStdev) * plateScale;
+    float astError = 0.5*(results->xStats->robustStdev + results->yStats->robustStdev) * plateScale;
+    int astNstar = results->yStats->clippedNvalues;
+
+    bool validSolution = true;
+
+    // XXX should these result in errors or be handled another way?
+    psLogMsg ("psastro", PS_LOG_INFO, "astrometry solution: error: %f arcsec, Nstars: %d", astError, astNstar);
+    if (astError > maxError) {
+        psLogMsg("psastro", PS_LOG_INFO, "residual error is too large, failed to find a solution: %f > %f", astError, maxError);
+	validSolution = false;
+    }
+    if (astNstar < minNstar) {
+        psLogMsg("psastro", PS_LOG_INFO, "solution uses too few stars: %d < %d", astNstar, minNstar);
+	validSolution = false;
+    }
+
+    // DVO expects NASTRO = 0 if we fail to find a solution
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "PERROR",   PS_META_REPLACE, "astrometry error (pixels)", pixError);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "CERROR",   PS_META_REPLACE, "astrometry error (arcsec)", astError);
+    if (validSolution) {
+	psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", astError/sqrt(astNstar));
+	psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", astNstar);
+    } else {
+	psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", 0.0);
+	psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", 0);
+    }
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "EQUINOX",  PS_META_REPLACE, "equinox of ref catalog", 2000.0); // XXX this is bogus: should be defined based on equinox of refstars
+
+    // XXX drop from here : determine fromFPA transformation and apply new transformation to raw & ref stars
+    // psastroUpdateChipToFPA (fpa, chip, rawstars, refstars);
+    
+    // XXX check if we correctly applied the new transformation:
+    if (psTraceGetLevel("psastro.dump") > 0) {
+	psastroDumpRawstars (rawstars, fpa, chip);
+	psastroDumpMatchedStars ("match.dat", rawstars, refstars, match);
+	psastroDumpStars (refstars, "refstars.cal.dat");
+    }
+
+    if (psTraceGetLevel("psastro.plot") > 0) {
+	psastroPlotOneChipFit (rawstars, refstars, match, recipe);
+    }
+
+    psFree (match);
+    psFree (stats);
+    psFree (results);
+    psFree (fitStats);
+    return validSolution;
+}
+
+// psastroWriteStars ("raw.1.dat", rawstars);
+// psastroWriteStars ("ref.1.dat", refstars);
+// psastroWriteTransform (chip->toFPA);
+
Index: /branches/eam_branch_20071023/psastro/src/psastroParseCamera.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroParseCamera.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroParseCamera.c	(revision 15358)
@@ -0,0 +1,37 @@
+# include "psastroInternal.h"
+
+bool psastroParseCamera (pmConfig *config) {
+
+    bool status = false;
+
+    // the input image(s) are required arguments; they define the camera
+    pmFPAfile *input = pmFPAfileDefineFromArgs (&status, config, "PSASTRO.INPUT", "INPUT");
+    if (!status) {
+	psError(PSASTRO_ERR_CONFIG, false, "Failed to build FPA from PSASTRO.INPUT");
+	return false;
+    }
+
+    // define the additional input/output files associated with psphot 
+    if (!psastroDefineFiles (config, input)) {
+	psError(PSASTRO_ERR_CONFIG, false, "Trouble defining the additional input/output files");
+	return false;
+    }
+
+    // Chip selection: turn on only the chips specified (option is not required)
+    char *chipLine = psMetadataLookupStr(&status, config->arguments, "CHIP_SELECTIONS"); 
+    psArray *chips = psStringSplitArray (chipLine, ",", false);
+    if (chips->n > 0) {
+	pmFPASelectChip (input->fpa, -1, true); // deselect all chips
+	for (int i = 0; i < chips->n; i++) {
+	    int chipNum = atoi(chips->data[i]);
+	    if (! pmFPASelectChip(input->fpa, chipNum, false)) {
+		psError(PSASTRO_ERR_CONFIG, true, "Chip number %d doesn't exist in camera.\n", chipNum);
+		return false;
+	    }
+        }
+    }
+    psFree (chips);
+
+    psTrace("psastro", 1, "Done with psastroParseCamera...\n");
+    return true;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroRefstarSubset.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroRefstarSubset.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroRefstarSubset.c	(revision 15358)
@@ -0,0 +1,90 @@
+# include "psastroInternal.h"
+
+bool psastroRefstarSubset (pmReadout *readout) {
+
+  // select the raw objects for this readout
+  psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+  if (rawstars == NULL)  {
+    psError(PSASTRO_ERR_DATA, false, "missing rawstars in psastroRefstarSubset\n");
+    return false;
+  }
+
+  // select the raw objects for this readout
+  psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+  if (refstars == NULL)  {
+    psError(PSASTRO_ERR_DATA, false, "missing refstars in psastroRefstarSubset\n");
+    return false;
+  }
+
+  // calculate luminosity functions for rawstars and refstars
+  // the samples cover the same area (the chip), so no area correction
+  // is needed...
+  psLogMsg ("psastro", 4, "measuring luminosity function for rawstars\n");
+  pmLumFunc *rawfunc = psastroLuminosityFunction (rawstars);
+  if (rawfunc == NULL) {
+    psLogMsg ("psastro", 4, "giving up on rawstars for this readout\n");
+    return true;
+  }
+  psLogMsg ("psastro", 4, "measuring luminosity function for refstars\n");
+  pmLumFunc *reffunc = psastroLuminosityFunction (refstars);
+  if (reffunc == NULL) {
+    psLogMsg ("psastro", 4, "giving up on refstars for this readout\n");
+    return true;
+  }
+
+// XXX code to better deal with mismatches in the fitted lum function
+# if (0)
+  // if the fitted slopes differ by too much, give up and just try to match the peak bin
+  fSlope = (reffunc->slope / rawfunc->slope);
+  if ((fSlope > 1.3) || (fSlope < 0.77)) {
+      // XXX do something here (choose the peak of the smaller set, generate a histogram for
+      // the other set, then choose the bin from the larger set which has nBin = nPeak
+  }
+# endif
+
+  // what is the offset between the two lines at the average magnitude?
+  double mRef = 0.5*(reffunc->mMin + reffunc->mMax);
+  double logRho = mRef * reffunc->slope + reffunc->offset;
+  double mRaw = (logRho - rawfunc->offset) / rawfunc->slope;
+
+  psLogMsg ("psastro", 4, "mRef: %f, logRho: %f, mRaw: %f\n", mRef, logRho, mRaw);
+
+  double mRefMax = rawfunc->mMax - mRaw + mRef;
+  psLogMsg ("psastro", 4, "clipping stars fainter than %f\n", mRefMax);
+
+  psArray *subset = psArrayAllocEmpty (100);
+  for (int i = 0; i < refstars->n; i++) {
+    pmAstromObj *ref = refstars->data[i];
+    if (ref->Mag > mRefMax) continue;
+    psArrayAdd (subset, 100, ref);
+  }
+
+  psLogMsg ("psastro", 4, "keeping %ld of %ld reference stars\n", subset->n, refstars->n);
+
+  psMetadataRemoveKey (readout->analysis, "PSASTRO.REFSTARS");
+  psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.REFSTARS", PS_DATA_ARRAY, "astrometry matches", subset);
+
+  if (psTraceGetLevel("psastro.dump") > 0) {
+      pmChip *chip = readout->parent->parent;
+
+      char *filename = NULL;
+      char *chipname = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+      psStringAppend (&filename, "refstars.%s.dat", chipname);
+      psastroDumpRefstars (subset, filename);
+      psFree (filename);
+  }
+
+  psFree (rawfunc);
+  psFree (reffunc);
+  psFree (subset);
+
+  return true;
+}
+
+/* this test is a bit sensitive to the total number of refstars or rawstars available
+   watch out if:
+   - the fitted slopes are extremely different 
+   - the average number of stars per bin is ~1
+   
+   skip the cut in these cases?
+*/
Index: /branches/eam_branch_20071023/psastro/src/psastroStandAlone.h
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroStandAlone.h	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroStandAlone.h	(revision 15358)
@@ -0,0 +1,19 @@
+# ifdef HAVE_CONFIG_H
+# include <config.h>
+# endif
+
+#ifndef PSASTRO_STAND_ALONE_H
+#define PSASTRO_STAND_ALONE_H
+
+#include <stdio.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "psastro.h"
+
+// Top level functions
+pmConfig         *psastroArguments (int argc, char **argv);
+void              psastroCleanup (pmConfig *config);
+bool              psastroParseCamera (pmConfig *config);
+bool              psastroDataLoad (pmConfig *config);
+
+#endif
Index: /branches/eam_branch_20071023/psastro/src/psastroTestFuncs.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroTestFuncs.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroTestFuncs.c	(revision 15358)
@@ -0,0 +1,45 @@
+# include "psastroInternal.h"
+
+// write out objects
+bool psastroWriteStars (char *filename, psArray *sources) {
+
+    // re-open, add data to end of file
+    FILE *f = fopen (filename, "w");
+    if (f == NULL) {
+	psLogMsg ("psastroWriteStars", 3, "can't open output file for output %s\n", filename);
+	return false;
+    }
+
+    for (int i = 0; i < sources->n; i++) {
+	
+	pmAstromObj *star = sources->data[i];
+
+	fprintf (f, "%8.2f %8.2f   %8.2f %8.2f   %8.2f %8.2f   %10.6f %10.6f   %8.2f %8.2f\n", 
+		 star->chip->x, star->chip->y, 
+		 star->FP->x, star->FP->y, 
+		 star->TP->x, star->TP->y, 
+		 star->sky->r*DEG_RAD, star->sky->d*DEG_RAD, 
+		 star->Mag, star->dMag);
+    }
+    fclose (f);
+    return true;
+}
+
+bool psastroWriteTransform (psPlaneTransform *map) {
+
+    // dump initial values:
+    for (int i = 0; i < map->x->nX + 1; i++) {
+	for (int j = 0; j < map->x->nY + 1; j++) {
+	    if (map->x->coeffMask[i][j] & PS_POLY_MASK_SET) continue;
+	    psLogMsg ("psastro", 4, "x term %d,%d: %f +/- %f\n", i, j, map->x->coeff[i][j], map->x->coeffErr[i][j]);
+	}
+    }
+
+    for (int i = 0; i < map->y->nX + 1; i++) {
+	for (int j = 0; j < map->y->nY + 1; j++) {
+	    if (map->y->coeffMask[i][j] & PS_POLY_MASK_SET) continue;
+	    psLogMsg ("psastro", 4, "y term %d,%d: %f +/- %f\n", i, j, map->y->coeff[i][j], map->y->coeffErr[i][j]);
+	}
+    }
+    return true;
+}
Index: /branches/eam_branch_20071023/psastro/src/psastroUtils.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroUtils.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroUtils.c	(revision 15358)
@@ -0,0 +1,217 @@
+# include "psastroInternal.h"
+# define RENORM 0
+
+// fix this to look up the value in the chip concepts
+static double getChipPixelScale (pmChip *chip) {
+    return 10.0;
+}
+
+// I have an FPA structure with multiple chips.  we have loaded or measured astrometry for each
+// chip with independent pixel/degree scaling values.  These will naturally compensate locally
+// somewhat for the telescope distortion.  to measure the telescope distortion, we need to
+// force the chips to have the same pixel scale and measure the difference from that solution.
+// Convert an FPA with disparate pixel scales to a common pixel scale (perhaps depending on the
+// chip -- eg, TC3)
+
+bool psastroMosaicCommonScale (pmFPA *fpa, psMetadata *recipe) {
+
+    // options : use the MIN or MAX chip as global reference or supplied pixel scales
+    // (microns/pixel), which may depend on the chip
+
+    float pixelScaleUse = 1.0, pixelScale1 = 1.0,  pixelScale2 = 1.0,  pixelScale = 1.0;
+
+    char *option = psMetadataLookupStr (NULL, recipe, "PSASTRO.COMMON.SCALE.OPTION");
+    if (option == NULL) {
+	psError(PSASTRO_ERR_DATA, false, "no choice set for common scale option\n");
+	return false;
+    }
+
+    bool useExternal = true;
+
+    // find the min or max scale chip
+    if (!strcasecmp (option, "MIN") || !strcasecmp (option, "MAX")) {
+
+	bool useMax = !strcasecmp (option, "MAX");
+	pixelScaleUse = (useMax) ? FLT_MIN : FLT_MAX;
+
+	for (int i = 0; i < fpa->chips->n; i++) {
+	    pmChip *chip = fpa->chips->data[i];
+	    if (!chip->process || !chip->file_exists) { continue; }
+	    if (!chip->toFPA) { continue; }
+
+	    pixelScale1 = hypot (chip->toFPA->x->coeff[1][0], chip->toFPA->x->coeff[0][1]);
+	    pixelScale2 = hypot (chip->toFPA->y->coeff[1][0], chip->toFPA->y->coeff[0][1]);
+	    pixelScale = 0.5*(pixelScale1 + pixelScale2);
+	    
+	    pixelScaleUse = (useMax) ? PS_MAX (pixelScale, pixelScaleUse) : PS_MIN (pixelScale, pixelScaleUse);
+	}
+	useExternal = false;
+    }
+
+    // rescale each chip by the reference scale
+    for (int i = 0; i < fpa->chips->n; i++) {
+	pmChip *chip = fpa->chips->data[i];
+	if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->toFPA) { continue; }
+
+	psPlaneTransform *toFPA = chip->toFPA;
+	psPlaneTransform *fromFPA = chip->fromFPA;
+	    
+	pixelScale1 = hypot (toFPA->x->coeff[1][0], toFPA->x->coeff[0][1]);
+	pixelScale2 = hypot (toFPA->y->coeff[1][0], toFPA->y->coeff[0][1]);
+
+	if (useExternal) {
+	    pixelScaleUse = getChipPixelScale (chip);
+	}
+
+	for (int i = 0; i <= toFPA->x->nX; i++) {
+	    for (int j = 0; j <= toFPA->x->nX; j++) {
+		toFPA->x->coeff[i][j] *= pixelScaleUse/pixelScale1;
+		toFPA->y->coeff[i][j] *= pixelScaleUse/pixelScale2;
+		fromFPA->x->coeff[i][j] *= pixelScale1/pixelScaleUse;
+		fromFPA->y->coeff[i][j] *= pixelScale2/pixelScaleUse;
+	    }
+	}
+
+    }
+    psastroMosaicSetAstrom (fpa);
+    return true;
+}
+
+bool psastroUpdateChipToFPA (pmFPA *fpa, pmChip *chip, psArray *rawstars, psArray *refstars) {
+
+    psRegion *region = pmChipPixels (chip);
+
+    psFree (chip->fromFPA);
+    chip->fromFPA = psPlaneTransformInvert (NULL, chip->toFPA, *region, 50);
+    psFree (region);
+
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+
+        psPlaneTransformApply (raw->FP, chip->toFPA, raw->chip);
+        psPlaneTransformApply (raw->TP, fpa->toTPA, raw->FP);
+        psDeproject (raw->sky, raw->TP, fpa->toSky);
+    }
+
+    for (int i = 0; i < refstars->n; i++) {
+        pmAstromObj *ref = refstars->data[i];
+        psPlaneTransformApply (ref->chip, chip->fromFPA, ref->FP);
+    }
+
+    return true;
+}
+
+# if 0
+
+bool psastroSelectBrightStars (pmFPA *fpa, psMetadata *config) {
+
+    bool status;
+
+    // add exclusions for objects on some basis?
+    int MAX_NSTARS = psMetadataLookupS32 (&status, config, "PSASTRO.STARS.MAX");
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        for (int j = 0; j < chip->cells->n; j++) {
+            pmCell *cell = chip->cells->data[j];
+            for (int k = 0; k < cell->readouts->n; k++) {
+                pmReadout *readout = cell->readouts->data[k];
+
+                psArray *stars = psMetadataLookupPtr (&status, readout->analysis, "STARS.FULLSET");
+                stars = psArraySort (stars, pmAstromObjSortByMag);
+
+                int nSubset = PS_MIN (MAX_NSTARS, stars->n);
+                psArray *subset = psArrayAlloc (nSubset);
+
+                for (int i = 0; i < nSubset; i++) {
+                    subset->data[i] = stars->data[i];
+                }
+                psMetadataAdd (readout->analysis, PS_LIST_TAIL, "STARS.SUBSET", PS_DATA_ARRAY, "stars from analysis", subset);
+            }
+        }
+    }
+    return true;
+}
+
+psPlaneDistort *psPlaneDistortCopy (psPlaneDistort *input) {
+
+    psPlaneDistort *output = psPlaneDistortAlloc (input->x->nX, input->x->nY, input->x->nZ, input->x->nT);
+
+    for (int i = 0; i < input->x->nX; i++) {
+        for (int j = 0; j < input->x->nY; j++) {
+            for (int k = 0; k < input->x->nZ; k++) {
+                for (int m = 0; m < input->x->nT; m++) {
+                    // x-terms
+                    output->x->mask[i][j][k][m]     = input->x->mask[i][j][k][m];
+                    output->x->coeff[i][j][k][m]    = input->x->coeff[i][j][k][m];
+                    output->x->coeffErr[i][j][k][m] = input->x->coeffErr[i][j][k][m];
+                    // y-terms
+                    output->y->mask[i][j][k][m]     = input->y->mask[i][j][k][m];
+                    output->y->coeff[i][j][k][m]    = input->y->coeff[i][j][k][m];
+                    output->y->coeffErr[i][j][k][m] = input->y->coeffErr[i][j][k][m];
+                }
+            }
+        }
+    }
+    return (output);
+}
+
+psPlaneTransform *psPlaneTransformCopy (psPlaneTransform *input) {
+
+    psPlaneTransform *output = psPlaneTransformAlloc (input->x->nX, input->x->nY);
+
+    for (int i = 0; i < input->x->nX; i++) {
+        for (int j = 0; j < input->x->nY; j++) {
+            // x-terms
+            output->x->mask[i][j]     = input->x->mask[i][j];
+            output->x->coeff[i][j]    = input->x->coeff[i][j];
+            output->x->coeffErr[i][j] = input->x->coeffErr[i][j];
+            // y-terms
+            output->y->mask[i][j]     = input->y->mask[i][j];
+            output->y->coeff[i][j]    = input->y->coeff[i][j];
+            output->y->coeffErr[i][j] = input->y->coeffErr[i][j];
+        }
+    }
+    return (output);
+}
+
+psProjection *psProjectionCopy (psProjection *input) {
+
+    psProjection *output = psProjectionAlloc (input->R, input->D, input->Xs, input->Ys, input->type);
+    return (output);
+}
+
+// returns the rotation term, forcing positive parity
+double psPlaneTransformGetRotation (psPlaneTransform *map) {
+
+    if (map->x->nX < 1) return 0;
+    if (map->x->nY < 1) return 0;
+
+    if (map->y->nX < 1) return 0;
+    if (map->y->nY < 1) return 0;
+
+    double pc1_1 = map->x->coeff[1][0];
+    double pc1_2 = map->x->coeff[0][1];
+    double pc2_1 = map->y->coeff[1][0];
+    double pc2_2 = map->y->coeff[0][1];
+
+    double px = SIGN (pc1_1);
+    double py = SIGN (pc2_2);
+
+    // both x and y terms imply an angle. take the average
+    double t1 = -atan2 (px*pc1_2, px*pc1_1);
+    double t2 = +atan2 (py*pc2_1, py*pc2_2);
+
+    // careful near -pi,+pi boundary...
+    if (t1 - t2 > M_PI/2) t2 += 2*M_PI;
+    if (t2 - t1 > M_PI/2) t1 += 2*M_PI;
+
+    double theta = 0.5*(t1 + t2);
+    while (theta < M_PI) theta += 2*M_PI;
+    while (theta > M_PI) theta -= 2*M_PI;
+
+    return (theta);
+}
+
+# endif
Index: /branches/eam_branch_20071023/psastro/src/psastroVersion.c
===================================================================
--- /branches/eam_branch_20071023/psastro/src/psastroVersion.c	(revision 15358)
+++ /branches/eam_branch_20071023/psastro/src/psastroVersion.c	(revision 15358)
@@ -0,0 +1,20 @@
+#include "psastroInternal.h"
+
+static const char *cvsTag = "$Name: not supported by cvs2svn $";// CVS tag name
+
+psString psastroVersion(void)
+{
+    psString version = NULL;            // Version, to return
+    psStringAppend(&version, "%s-%s",PACKAGE_NAME,PACKAGE_VERSION);
+    return version;
+}
+
+psString psastroVersionLong(void)
+{
+    psString version = psastroVersion(); // Version, to return
+    psString tag = psStringStripCVS(cvsTag, "Name"); // CVS tag
+    psStringAppend(&version, " (cvs tag %s) %s, %s", tag, __DATE__, __TIME__);
+    psFree(tag);
+    return version;
+}
+
Index: /branches/eam_branch_20071023/psphot/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psphot/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/.cvsignore	(revision 15358)
@@ -0,0 +1,19 @@
+bin
+Makefile
+Makefile.in
+aclocal.m4
+autom4te.cache
+compile
+config.log
+config.status
+configure
+depcomp
+install-sh
+missing
+config.guess
+libtool
+ltmain.sh
+stamp-h1
+config.sub
+psphot.pc
+psphot-config
Index: /branches/eam_branch_20071023/psphot/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psphot/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/Makefile.am	(revision 15358)
@@ -0,0 +1,12 @@
+SUBDIRS = src
+
+CLEANFILES = *.pyc *~ core core.*
+
+bin_SCRIPTS = psphot-config 
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA= psphot.pc
+
+EXTRA_DIST = \
+	psphot-config.in \
+	psphot.pc.in \
+	autogen.sh
Index: /branches/eam_branch_20071023/psphot/autogen.sh
===================================================================
--- /branches/eam_branch_20071023/psphot/autogen.sh	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/autogen.sh	(revision 15358)
@@ -0,0 +1,103 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+srcdir=`dirname $0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+cd $srcdir
+
+PROJECT=psphot
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL="aclocal $ACLOCAL_FLAGS"
+AUTOHEADER=autoheader
+AUTOMAKE=automake
+AUTOCONF=autoconf
+
+($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $LIBTOOlIZE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/libtool/"
+        DIE=1
+}
+
+($ACLOCAL --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $ACLOCAL installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOHEADER --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOHEADER installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOMAKE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOCONF --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOCONF installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+if test "$DIE" -eq 1; then
+        exit 1
+fi
+
+test $TEST_TYPE $FILE || {
+        echo "You must run this script in the top-level $PROJECT directory"
+        exit 1
+}
+
+if test -z "$*"; then
+        echo "I am going to run ./configure with no arguments - if you wish "
+        echo "to pass any to it, please specify them on the $0 command line."
+fi
+
+$LIBTOOLIZE --copy --force || echo "$LIBTOOlIZE failed"
+$ACLOCAL || echo "$ACLOCAL failed"
+$AUTOHEADER || echo "$AUTOHEADER failed"
+$AUTOMAKE --add-missing --force-missing --copy || echo "$AUTOMAKE  failed"
+$AUTOCONF || echo "$AUTOCONF failed"
+
+cd $ORIGDIR
+
+run_configure=true
+for arg in $*; do
+    case $arg in
+        --no-configure)
+            run_configure=false
+            ;;
+        *)
+            ;;
+    esac
+done
+
+if $run_configure; then
+    $srcdir/configure --enable-maintainer-mode "$@"
+    echo
+    echo "Now type 'make' to compile $PROJECT."
+else
+    echo
+    echo "Now run 'configure' and 'make' to compile $PROJECT."
+fi
Index: /branches/eam_branch_20071023/psphot/configure.ac
===================================================================
--- /branches/eam_branch_20071023/psphot/configure.ac	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/configure.ac	(revision 15358)
@@ -0,0 +1,216 @@
+dnl Process this file with autoconf to produce a configure script.
+AC_PREREQ(2.61)
+
+AC_INIT([psphot], [0.9.0], [ipp-support@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([src])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+AM_CONFIG_HEADER([src/config.h])
+AM_MAINTAINER_MODE
+
+IPP_STDCFLAGS
+
+AC_LANG(C)
+AC_GNU_SOURCE
+AC_PROG_CC_C99
+AC_PROG_INSTALL
+AC_PROG_LIBTOOL
+AC_SYS_LARGEFILE
+
+dnl ------------------------------------------------------------
+
+AC_PATH_PROG([ERRORCODES], [psParseErrorCodes], [missing])
+if test "$ERRORCODES" = "missing" ; then
+  AC_MSG_ERROR([psParseErrorCodes is required])
+fi
+
+dnl ------------------ kapa,libkapa options -------------------------
+dnl -- libkapa implies the requirement for libpng, libjpeg as well --
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+dnl test for command-line options: use ohana-config if not supplied
+KAPA_CFLAGS_CONFIG="true"
+KAPA_LIBS_CONFIG="true"
+AC_ARG_WITH(kapa,
+[AS_HELP_STRING(--with-kapa=DIR,Specify location of libkapa)],
+[KAPA_CFLAGS="-I$withval/include" KAPA_LIBS="-L$withval/lib" 
+ KAPA_CFLAGS_CONFIG="false"       KAPA_LIBS_CONFIG="false"])
+AC_ARG_WITH(kapa-include,
+[AS_HELP_STRING(--with-kapa-include=DIR,Specify libkapa include directory.)],
+[KAPA_CFLAGS="-I$withval" KAPA_CFLAGS_CONFIG="false"])
+AC_ARG_WITH(kapa-lib,
+[AS_HELP_STRING(--with-kapa-lib=DIR,Specify libkapa library directory.)],
+[KAPA_LIBS="-L$withval" KAPA_LIBS_CONFIG="false"])
+
+echo "KAPA_CFLAGS_CONFIG: $KAPA_CFLAGS_CONFIG"
+echo "KAPA_LIBS_CONFIG: $KAPA_LIBS_CONFIG"
+echo "KAPA_CFLAGS: $KAPA_CFLAGS"
+echo "KAPA_LIBS: $KAPA_LIBS"
+
+dnl HAVE_KAPA is set to false if any of the tests fail
+HAVE_KAPA="true"
+AC_MSG_NOTICE([checking for libkapa])
+if test "$KAPA_CFLAGS_CONFIG" = "true" -o "$KAPA_LIBS_CONFIG" = "true"; then
+  AC_MSG_NOTICE([kapa info supplied by ohana-config])
+  KAPA_CONFIG=`which ohana-config`
+  AC_CHECK_FILE($KAPA_CONFIG,[],
+    [HAVE_KAPA="false"; AC_MSG_WARN([libkapa is not found: output plots disabled.  Obtain libkapa at http://kiawe.ifa.hawaii.edu/Elixir/Ohana or use --with-kapa to specify location])])
+  
+  echo "HAVE_KAPA: $HAVE_KAPA"
+  echo "KAPA_CFLAGS_CONFIG: $KAPA_CFLAGS_CONFIG"
+
+  if test "$HAVE_KAPA" = "true" -a "$KAPA_CFLAGS_CONFIG" = "true" ; then
+   AC_MSG_NOTICE([libkapa cflags info supplied by ohana-config])
+   AC_MSG_CHECKING([libkapa cflags])
+   KAPA_CFLAGS="`${KAPA_CONFIG} --cflags`"
+   AC_MSG_RESULT([${KAPA_CFLAGS}])
+  fi
+
+  if test "$HAVE_KAPA" = "true" -a "$KAPA_LIBS_CONFIG" = "true" ; then
+   AC_MSG_NOTICE([libkapa ldflags info supplied by ohana-config])
+   AC_MSG_CHECKING([libkapa ldflags])
+   KAPA_LIBS="`${KAPA_CONFIG} --libs` -lX11"
+   AC_MSG_RESULT([${KAPA_LIBS}])
+  fi
+fi
+
+if test "$HAVE_KAPA" = "true" ; then
+ AC_MSG_NOTICE([libkapa supplied])
+ PSPHOT_CFLAGS="${PSPHOT_CFLAGS} ${KAPA_CFLAGS}"
+ PSPHOT_LIBS="${PSPHOT_LIBS} ${KAPA_LIBS}"
+else
+ AC_MSG_NOTICE([libkapa ignored])
+fi
+
+dnl HAVE_KAPA is set to false if any of the tests fail
+dnl HAVE_KAPA=true
+dnl AC_CHECK_HEADERS([kapa.h],
+dnl  [PSPHOT_CFLAGS="$PSPHOT_CFLAGS $KAPA_CFLAGS" AC_SUBST(KAPA_CFLAGS)],
+dnl  [HAVE_KAPA=false; AC_MSG_WARN([libkapa headers not found: output plots disabled.  Obtain libkapa at http://kiawe.ifa.hawaii.edu/Elixir/Ohana or use --with-kapa to specify location.])]
+dnl )
+dnl AC_CHECK_LIB(kapa,KapaInitGraph,
+dnl  [PSPHOT_LIBS="$PSPHOT_LIBS $JPEG_LDFLAGS -ljpeg"],  
+dnl  [HAVE_KAPA=false; AC_MSG_WARN([libkapa headers not found: output plots disabled.  Obtain libkapa at http://kiawe.ifa.hawaii.edu/Elixir/Ohana or use --with-kapa to specify location.])],[-lm]
+dnl )
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ libjpeg options ---------------------
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+AC_ARG_WITH(jpeg,
+[AS_HELP_STRING(--with-jpeg=DIR,Specify location of libjpeg.)],
+[JPEG_CFLAGS="-I$withval/include"
+ JPEG_LDFLAGS="-L$withval/lib"])
+AC_ARG_WITH(jpeg-include,
+[AS_HELP_STRING(--with-jpeg-include=DIR,Specify libjpeg include directory.)],
+[JPEG_CFLAGS="-I$withval"])
+AC_ARG_WITH(jpeg-lib,
+[AS_HELP_STRING(--with-jpeg-lib=DIR,Specify libjpeg library directory.)],
+[JPEG_LDFLAGS="-L$withval"])
+
+CFLAGS="${CFLAGS} ${JPEG_CFLAGS}"
+CPPFLAGS=${CFLAGS}
+LDFLAGS="${LDFLAGS} ${JPEG_LDFLAGS}"
+
+AC_CHECK_HEADERS([jpeglib.h],
+  [PSPHOT_CFLAGS="$PSPHOT_CFLAGS $JPEG_CFLAGS" AC_SUBST(JPEG_CFLAGS)],
+  [HAVE_KAPA=false; AC_MSG_WARN([libjpeg headers not found: output plots disabled.  Obtain libjpeg from http://www.ijg.org/ or use --with-jpeg to specify location.])]
+)
+
+AC_CHECK_LIB(jpeg,jpeg_CreateCompress,
+  [PSPHOT_LIBS="$PSPHOT_LIBS $JPEG_LDFLAGS -ljpeg"],
+  [HAVE_KAPA=false; AC_MSG_WARN([libjpeg library not found: output plots disabled.  Obtain libjpeg from http://www.ijg.org/ or use --with-jpeg to specify location.])]
+)
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ libpng options ---------------------
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+AC_ARG_WITH(png,
+[AS_HELP_STRING(--with-png=DIR,Specify location of libpng.)],
+[PNG_CFLAGS="-I$withval/include"
+ PNG_LDFLAGS="-L$withval/lib"])
+AC_ARG_WITH(png-include,
+[AS_HELP_STRING(--with-png-include=DIR,Specify libpng include directory.)],
+[PNG_CFLAGS="-I$withval"])
+AC_ARG_WITH(png-lib,
+[AS_HELP_STRING(--with-png-lib=DIR,Specify libpng library directory.)],
+[PNG_LDFLAGS="-L$withval"])
+
+CFLAGS="${CFLAGS} ${PNG_CFLAGS}"
+CPPFLAGS=${CFLAGS}
+LDFLAGS="${LDFLAGS} ${PNG_LDFLAGS}"
+
+AC_CHECK_HEADERS([png.h],
+  [PSPHOT_CFLAGS="$PSPHOT_CFLAGS $PNG_CFLAGS" AC_SUBST(PNG_CFLAGS)],
+  [HAVE_KAPA=false; AC_MSG_WARN([libpng headers not found: output plots disabled.  Obtain libpng from http://www.ijg.org/ or use --with-png to specify location.])]
+)
+
+AC_CHECK_LIB(png,png_init_io,
+  [PSPHOT_LIBS="$PSPHOT_LIBS $PNG_LDFLAGS -lpng"],
+  [HAVE_KAPA=false; AC_MSG_WARN([libpng library not found: output plots disabled.  Obtain libpng from http://www.ijg.org/ or use --with-png to specify location.])]
+)
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ use kapa or not? ---------------------
+
+if test "$HAVE_KAPA" == "true" ; then
+  AC_MSG_RESULT([including plotting functions])
+  AC_DEFINE([HAVE_KAPA],[1],[enable use of libkapa])
+else
+  AC_MSG_RESULT([skipping plotting functions])
+  AC_DEFINE([HAVE_KAPA],[0],[disable use of libkapa])
+fi
+
+dnl ------------- psLib, psModules ---------------
+PKG_CHECK_MODULES([PSLIB], [pslib >= 1.0.0])
+PKG_CHECK_MODULES([PSMODULE], [psmodules >= 1.0.0])
+
+dnl Set CFLAGS for build
+IPP_STDOPTS
+
+CFLAGS="${CFLAGS=} -Wall -Werror"
+echo "PSPHOT_CFLAGS: $PSPHOT_CFLAGS"
+echo "PSPHOT_LIBS: $PSPHOT_LIBS"
+
+AC_SUBST([PSPHOT_CFLAGS])
+AC_SUBST([PSPHOT_LIBS])
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+  psphot-config
+  psphot.pc
+])
+
+AC_OUTPUT
Index: /branches/eam_branch_20071023/psphot/doc/2Dmodels.txt
===================================================================
--- /branches/eam_branch_20071023/psphot/doc/2Dmodels.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/doc/2Dmodels.txt	(revision 15358)
@@ -0,0 +1,29 @@
+
+I am testing a new concept for the 2D representation of various values
+measured by psphot.  I have been using 2D polynomials to fit the
+variations of, eg, the psf parameters as a function of position in the
+images.  this has been somewhat successful, but it has also been
+fraught with a number of problems.  the worst issue has been the fact
+that the polynomial form is not physically motivated.  in some cases,
+eg the variation of the psf shape parameters, the true variations for
+real distributions are not well represented by a polynomial.  I have
+done some ad hoc tricks to get the functional form to look roughly
+like a polynomial, but this is really quite a hack.  the other problem
+is that it is difficult to decide if the polynomial does an acceptable
+job representing the variations.
+
+My new concept for modeling 2D variations in some parameter is to
+define a low-resolution 2D image to represent the variation.  The
+advantage is that the image may be define with whatever resolution may
+be sampled by the input data, and the functional form need not be
+constrained by the polynomial basis function.  
+
+** is this a poor substitute for using a chebychev polynomial as a
+   better basis function?
+
+psphot parameters which vary by position:
+
+pmPSF.params 
+pmPSF.ApTrend
+pmPSF.FluxScale (flux given normalization of 1.0)
+
Index: /branches/eam_branch_20071023/psphot/doc/config.txt
===================================================================
--- /branches/eam_branch_20071023/psphot/doc/config.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/doc/config.txt	(revision 15358)
@@ -0,0 +1,56 @@
+
+# input data options
+# IMAGE             STR    file.fits
+# MASK_IMAGE	    STR    mask.fits
+# WEIGHT_IMAGE	    STR	   weight.fits
+
+# output data options
+OUTPUT_FILE	    STR	   output.smp    # output object file
+OUTPUT_MODE	    STR	   TEXT          # output mode: TEXT, OBJ, SX, CMP, CMF 
+# RESID_IMAGE	    STR	   resid.fits    # output residual image
+
+# image noise parameters
+RDNOISE             STR  HD:RDNOISE      # read-noise in electrons
+GAIN                STR  HD:GAIN         # electrons / DN
+
+# masking parameters
+XMIN                F32  32        	 # minimum valid x-coord
+XMAX                F32   0        	 # maximum valid x-coord
+YMIN                F32   1        	 # minimum valid y-coord
+YMAX                F32   0        	 # maximum valid y-coord
+SATURATION          F32  50000     	 # saturation level on this chip
+
+# image statistics parameters
+IMSTATS_NPIX        S32  100000    	 # number of pixels to use for sky estimate:
+
+# peak finding 
+PEAKS_SMOOTH_SIGMA  F32  1.0       	 # smoothing kernel sigma in pixels
+PEAKS_SMOOTH_NSIGMA F32  3.0   	   	 # smoothing kernel width in sigmas
+PEAKS_NSIGMA_LIMIT  F32  10.0  	   	 # peak significance threshold
+
+# basic object statistics
+SKY_INNER_RADIUS    F32  15		 # square annulus for local sky measurement
+SKY_OUTER_RADIUS    F32  25		 # square annulus for local sky measurement
+PSF_MOMENTS_RADIUS  F32  5
+PSF_SN_LIM          F32  100
+
+# PSF model parameters : choose the PSF model
+# list as many PSF_MODEL options as desired
+PSF_MODEL           STR  PS_MODEL_QGAUSS
+#PSF_MODEL           STR  PS_MODEL_PGAUSS
+#PSF_MODEL           STR  PS_MODEL_GAUSS
+PSF_FIT_RADIUS      F32  25		 # fitting radius for test PSF model
+
+# PSF model parameters : apply the PSF model
+PSF_FIT_NSIGMA       F32  1		 # significance for pixel included in fit
+PSF_FIT_PADDING      F32  5              # extra annulus to use for fit 
+PSF_SHAPE_NSIGMA     F32  3.0		 # max significance for shape variation
+PSF_MIN_SN           F32  2.0		 # reject objects below this significance
+PSF_MAX_CHI          F32  10.0		 # reject objects worse that this
+
+# Galaxy model parameters
+GAL_MODEL            STR  PS_MODEL_SGAUSS
+GAL_MIN_SN           F32  3
+GAL_FIT_NSIGMA       F32  1		 # significance for pixel included in fit
+GAL_FIT_PADDING      F32  5              # extra annulus to use for fit 
+GAL_MOMENTS_RADIUS   F32  9
Index: /branches/eam_branch_20071023/psphot/doc/notes.txt
===================================================================
--- /branches/eam_branch_20071023/psphot/doc/notes.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/doc/notes.txt	(revision 15358)
@@ -0,0 +1,638 @@
+
+2007.08.17
+
+ I am working on a number of cleanup / fixes.  I have made an overhaul
+ of the pmModel APIs, adding function pointers in the pmModel
+ structure to the class-specific utility functions (eg,
+ pmModel->modelFunc is the actual function which is evaluated). 
+
+ TO DO:
+
+ * update pmSourceFitSet to be able to include more than one model
+   type (currently it assumes the sources are all, eg, PSFs).  This is
+   now needed because the old implementation used the function lookups
+   which have been dropped.
+
+ * define a generic API set to handle 2D modelling of a scalar using
+   either polynomials (as the pmPSF code currently does) or an
+   image-based representation (as the psphot sky model currently
+   does).  
+
+   The image-based representation can automatically step down from NxM
+   super pixels to a smaller number based on the density of
+   measurements.  
+
+   Include ways to smooth and regularize the output result.
+
+   This mechanism can be applied to: psf parameters, aperture
+   residual, psf peak-to-flux variations, the psphot background
+   representation.
+
+ * generate and store the output radial profile for objects
+
+ * finish testing and incorportate the CR / EXT measurements
+
+ * adjustments to pixel center based on second derivatives : needed
+   for the sersic models.
+
+ * adjustments to pixel flux for extreme values (r ~ 0) : needed for
+   the sersic models.
+
+ * on psf stars : fall back on a Gaussian.
+
+ * OPTIMIZATIONS !!
+
+ * drop the model sky element : should only be in source->sky,dsky
+
+2006.11.16
+
+ensemble:
+
+  * first block: 
+    * select sources of interest / skip
+    * generate a guess model
+    * re-measure moments for saturated stars (MOVE)
+    * update peak position (MOVE to peaks)
+    * set the object radius 
+    * build temporary source copies (including pixels, mask, weight)
+
+  * second block:
+    * measure the cross-products and weights
+    * add to the sparse matrix
+
+  * build full-image mask
+  * measure coord weights
+  * build order elements
+
+  * solve for source amplitudes
+  * update models
+
+  * create psSparseBorder to solve matrix equations which have a large
+    sparse region, with a completely filled border of a smaller number
+    of rows.
+
+  * trying to modify psphotEnsemble to (1) be more general (not just
+    PSFs) and (2) fit for the sky offset.
+
+    - how is the radius for each source set?
+    
+
+2006.11.11
+
+  I am trying to define a complete set of tests to validate the
+  measurement of the photometry compared with the simulated images.
+  Part of my confusion is controlling exactly which corrections are
+  applied to the output magnitudes.  Here is the complete list:
+
+  psfMag_out = psfMag_meas + ApTrend(x,y,0.0,0.0)
+  apMag_out = apMag_meas + growth(radius,refRadius) + f(skyBias,skySat)
+
+  these corrections are modified by the following config settings:
+
+  IGNORE_GROWTH (sets all growth magnitudes to the fitMag value,
+  effectively turning off the growth correction)
+
+  APTREND : set the level of correction
+    NONE     : no correction is fitted
+    CONSTANT : just a single constant (applied to get psfMag_out)
+    SKYBIAS  : the skybias term is fitted (constant applied to psfMag,
+               slope applied apMag)
+  
+2006.11.08
+
+  I have been testing the pmSourceMagnitudes interpolation of the
+  source position when measuing the aperture magnitudes.  I built a
+  test suite for pmSourceMagnitudes and pmGrowthCurve.  even with
+  bicubic interpolation, there are noticable errors for small
+  apertures (ie, small compared to the PSF sigma).  I used a gaussian
+  PSF to make the tests.  it seems that, if the radius is less than 2
+  sigma or so, the interpolated aperture mag is in error by as much as
+  12mmag.  The size of this effect must depend on the smoothing
+  algorithm, the shape of the PSF, the size of the PSF relative to the
+  pixels, and the size of the subpixel interpolation.  I believe the
+  effect is a result of the smoothing introduced by the interpolation:
+  the smoothing pushes the flux in the inner portions of the PSF down
+  and enhances the flux in the outer portions.  thus, there is
+  actually a turn over in the error: at very large values, the error
+  is very small because the aperture contains all of the flux, and the
+  interpolation has little effect.  as the radius shrinks, the error
+  grows (in the sense of M_raw - M_interpolated; ie, the flux in the
+  interpolated source is too large).  around 2-3sigma, the error turns
+  over and becomes negative (too little flux in the interpolated
+  source). 
+
+  The sigmas I used for these tests (1.0, 1.5, 2.0 pix) are
+  appropriate for the range we expect to see from PS1, or any
+  well-sampled detector.  Thus, even though the effect is probably
+  less important for poorer seeing, we have to be careful to avoid it
+  in our Pan-STARRS analysis.
+
+  Currently, the minimum aperture is defined only by the config
+  variable PSF_FIT_PAD, which really implies a padding.  My proposal
+  is to require the minimum aperture to be a number of sigma (an
+  additional parameter).  Then, the formula for the radius would be
+  something like:
+
+  MAX (modelRadiusPSF (PSF_FIT_NSIGMA), PSF_FIT_RADIUS_NSIG) +
+  PSF_FIT_PAD
+
+  with PSF_FIT_RADIUS_NSIG (choose a better name!) set to something
+  like 2 or 3.
+
+2006.11.06
+
+  I have added the aperture interpolation as a component of the
+  pmSourceMagnitudes function.  
+
+  I am trying to make things a bit more organized, with more
+  consistent APIs. 
+
+  * changed pmPSFtry to use the pmSourceMagnitudes API
+  * changed pmPSFtry to use new pmSource entries for each try (saving
+  * the pmModels generated)
+
+  other notes:
+  * moved psphotGrowthCurve -> pmGrowthCurveGenerate 
+
+  things I know need to be added / fixed:
+  * measure bicube fit to peak from smoothed image
+  * use psphotEnsemble fit to re-measure normalization?
+  * extended object statistical measures 
+
+2006.11.02
+
+  Looking into the growth curve problem.  RHL sent his recipe for
+  correcting to true circular apertures.  I've done a test fix of
+  interpolating the stellar image to the desired center.  it looks
+  like bilinear interpolation is insufficient (about 8% error for 2
+  pixel radius aperture).  Unless I've got the formula wrong, the
+  effective smoothing of the gaussian is moving flux from the center
+  out and making small apertures in error that depends on the offset.
+  Bicubic could be enough to clean this up?  
+
+  I think the current code may need to be careful modified to include
+  the interpolation for every aperture measurement.  there are paired
+  'pmModelAdd' and pmModelSub steps to add and remove the model flux.
+  I need to be careful about how the mask affects these steps.
+
+  I also need to be careful in interpreting the test results: since
+  the simulated images create objects by filling in a pixel and then
+  smoothing, all objects have centroids which are exactly 0.5 pixel
+  offsets.  These all then have a fixed growth correction error, which
+  fortunately happens to be substantial (because of a 0.5 pixel offset
+  in the growth curve reference object!)
+
+  I need to double check the 0.5 pixel error issues.  There are
+  problems with the pslib version of psImageShift and
+  psImageInterpolate (or at least inconsistencies).  the pmModelAdd
+  and Sub functions and the centroid functions are probably also
+  inconsistent.
+
+2006.11.01
+
+  Trying to understand low-level errors in psphot using simulated
+  data.  the main issue I am concerned with is the bias RHL is seeing
+  in SDSS-psphot comparisons.  But there are other issues that are
+  creeping in and probably point at logical errors in the code.
+
+  * in pmPSFtry, I measure the first pass on the aperture residual (to
+    choose between PSF model options, if that is being tested).  This
+    analysis currently uses a polynomial fit (1st order) for the offset
+    and the sky bias (using r^2/flux as an independent variable).
+
+    Notes:
+
+    - is the psf-fit error distribution reasonable?  perhaps a little
+      on the high side: at 10000 DN, noise is 0.0138, should be
+      0.0107.
+
+    - scatter of ap-fit is surprisingly high.  M_input - M_fit has a
+      median chisq of 2 (mean of 5), while M_input - M_ap has a median
+      chisq of 125 (mean of 380).  This is using a radius of 15.  It
+      cleans up hugely when I go to a radius of 7 and even more so
+      with radius = 4. (In this case, the PSF sigma is 1.5, so 4 is
+      well matched and 7 is only slightly large).
+
+      * make the initial PSF fitting radius and aperture radius
+	  different?  
+      * make the initial PSF fitting radius a function of the measured
+	  PSF sigma?
+
+    - I have been fitting ap-fit mag without weights; the resulting
+      ap-fit errors are highly over-estimated since the ap-fit scatter
+      is too high.  I have added code to carry the weights (error from
+      the fit only at the moment, ie, not the ap error as well).  This
+      is good, but not as important if the radius is chosen well above.
+
+  * a bias in the psphotEnsemble results depending on the sky level
+    but not in the PSF model fits?  turning on/off CONST_PHOT_WT has
+    an equivalent effect, but is quite small.  why is one sensitive to
+    the sky and the other is not?  
+
+  * the error measured by psphotEnsemble was wrong (and inconsistent
+    for a given weighting situation): fixed the def of the error.
+
+  * still trying to understand the corrections being made.  running a
+    few tests:
+
+    keeping PSF_FIT_RADIUS at 4.0
+
+    t1 : no growth correction,    constant weights
+    t2 :    growth correction,    constant weights
+    t3 : no growth correction, no constant weights
+    t4 :    growth correction, no constant weights
+    t5 :    growth correction,    constant weights, constant ap (4)
+    t6 :    growth correction,    constant weights, integer radius
+    t7 :    growth correction,    constant weights, integer radius
+    (for t7, I've fixed the bug that the growth correction include the
+    min radius).
+
+  * psphotGrowthCurve is messed up : the measurement does not correct
+    for the actual object center, and at small radii this is an
+    important source of noise.
+
+2006.06.28
+
+  I am adding a few minor features.  First up is the ability to break
+  the processing at interesting stages.  These will be:
+
+  - PEAKS: after sources are generated, but before moments are
+    measured
+  - MOMENTS: after moments are measured, before the PSF is measured
+  - PSFMODEL: after the psf model is measured, before sources are
+    fitted in bulk
+  - ENSEMBLE: after the linear ensemble fitting
+  - DEFAULT: complete processing
+
+2006.04.22
+
+  I have updated psphot to work with the cycle 11 release of psLib and
+  psModules.  At this point, the IfA group is now working off of the
+  CVS head rather than from a separate IfA branch of psLib and
+  psModules.  The cycle 11 release of psLib needed a few updates to
+  fix minor problems.  The psModules code needed significant work to
+  fix discrepancies.  The new version of psphot has also been leak
+  checked, at least for basic configurations.  This new version of
+  psphot should be used with the new camera configuration system,
+  which can be found in the ipp/config directory.  Here are the
+  necessary tags:
+
+  psphot: psphot_dev_11_0
+  psModules: rel11_ifa_pre0
+  psLib: rel11_ifa_0
+
+2006.04.16
+
+  Examining some of the throughput issues.  the flux measurement speed
+  is strongly dependent on the interation of the function step size:
+
+  dz = 0.01 : 3.24s for 4400 objects
+  dz = 0.1  : 0.44s for 4400 objects
+
+  there is an increased error in the result with the larger step size,
+  but it is smaller than the phot error.  Still, we could do better
+  with a smarter integration function.
+
+  Including the aperture photometry on all sources, but not the
+  pixWeight, the source magnitudes takes 4.8 sec for 4400 objects.
+
+  Adding the pixWeight increases the time to 7.5 sec for 4400 objects
+
+2006.02.12
+
+  Last week, I made a new tag (psphot_dev_08) after finishing the work
+  to clean memory leaks and to modify psphot to work with Paul's
+  ppImage infrastructure.  
+
+  I have added a new background model function which generates a
+  median map based on Robert's suggestion of a) half-step windows and
+  b) linear interpolation.
+
+  Now, I am working to remove the constant sky level concepts and make
+  every relevant test use the signal/noise ratio as the trigger.  This
+  has impacts on the peak detections, the threshold for blends, and
+  the threshold for choosing pixels in the fitting routines.
+
+2005.12.23
+
+  Improvements in psphot as of psphot_dev_07
+
+  - updated to work with psModule release 9.0
+  - moved model test functions into psphot as optional mode
+  - added pmSourceFitSet : simultaneous model group fitting
+  - added bicubic interpolation to tweak up the peak coordinates
+  - added aperture residual analysis after complete object model & subtraction
+  - added 3D (x,y,rflux) aperture residual fitting
+  - added blend vs blob fitting (BlendFit)
+  - added ensemble PSF fitting, with sparse matrix inversion
+  - improved the flagging / marking of model results
+  - better abstraction for setting fit parameters
+  - various low-level fixes
+  - added PSF output option
+  - added break points
+  - added more optional output formats
+  - fixed FITS table output
+
+  Things still to be added to psphot:
+
+  - better initial background model (NxM median grid?)
+  - include local background model in EnsemblePSF Fit (use sparse matrix)
+  - 2nd pass (after object subtraction) to smooth and detect faint objects
+  - re-add background to object-subtracted image
+  - cleanup function names
+  - push more functions into psLib / psModule
+  - fix extended / galaxy models to improve stability
+  - add mode with input PSF
+  - add mode with input fixed sources (PS_SOURCE_FIXED)
+  - cleanup memory leaks
+
+2005.11.25
+
+  I've updated psphot to work with the current psLib v8, though a
+  number of psLib fixes were needed.  These are pushed under
+  psLib:eam_rel8_b2.  The psphot code which works with that version of
+  psLib is tagged psphot_dev_04.
+
+  I'm working on converting psphot to work with the current release of
+  psModules, which basically includes all of the object functions. 
+
+  - change comments are relative to psModules (vs psphot versions)
+
+  psEllipse.c : OK
+  psEllipse.h : OK
+  pmModelGroup.c : OK (missing TGAUSS entry)
+  pmModelGroup.h : OK (adds many comments)
+
+  pmPSFtry.c : a bunch of formatting weirdness (try-> got line breaks
+	       added pmPSFtryMetric_Alt
+	       fixed up usage of psVectorClipFitPolynomial1D  
+	       fixed up usage of psPolynomial..Alloc  
+
+  pmPSFtry.h : OK (Added extensive comments)
+
+  pmPSF.c : fixed up usage of psPolynomial..Alloc
+
+  pmPSF.h : OK (Added extensive comments)
+
+  pmObjects.c : cleaned up some formatting,
+		fixed usage of psImageCountPixelMask
+
+  pmObjects.h : added apMag and fitMag to pmSource
+		dropped PS_MODEL_name defines
+
+  psLibUtils.[ch] should be dropped from psModules
+  psModulesUtils.[ch] should be dropped from psModules
+
+  Makefile.am : dropped psModulesUtils.[ch], psLibUtils.[ch]
+
+  - I have successfully tested psphot with the psModules.v8 code
+    (eam_rel8_b1 tag).  I only needed to make a few modes in the
+    psModules code.  
+
+2005.11.16
+
+  I have made some fixes to make psphot work with pslib rel8_0.  I
+  need to merge psphot with the rel8_0 version of the psMinimize and
+  pmObjects code.  
+
+2005.09.06
+
+  I have built a working version of PSPhot using my own copy of the
+  pmObjects.[ch] files.  I need to minimize the difference between the
+  v0.7.0, v0.5.0, and my own version of pmObjects.c
+
+  additions to psLib based on the new version:
+  - extended the minimization to include parameter limits
+
+  changes v0.5.0 / v0.7.0 / EAM
+  - changed all ps* to pm*
+  - fixed naming for local static functions (eg modelFree not p_psModelFree)
+  - dropped all hard-coded model names (vs v0.7.0)
+  - using current psMemSetDellocator name
+  - added noise image to pmSource
+  - converted old asserts to new asserts (PS_.._CHECK..)
+  - add return from error in pmFindImagePeaks
+  - allow isItInThisRegion to have NULL region (true)
+  - fixed subImages to correspond to region definition (end is
+    exclusive) in LocalSky and SetPixelCircle
+  - dropped old, redundant code in pmSourceLocalSky
+  - fixed pmSourceMoments use of mask (0 is valid, not 1)
+  - added pmSourceMoments validity tests (numPixels, Sum, centroid shift)
+  - added correction for Sxy (XY/Sum - x*y)
+  - added pmSourcePSFClump function (and pmPSFClump structure)
+  - allow missing sources in pmSourcePSFClump (not all peaks yield
+    sources)
+  - moved pmSourceRoughClass metadata lookups out of inner loop
+  - added saturated pixel test using pmImageCountPixelMask
+  - various redefinitions of initial classes
+  - dropped old, redundant code in pmSourceSetPixelsCircle
+  - mask image set to type psU8 (was psF32)
+  - abstract object model functions for pmSourceModelGuess
+  - converted evalModel to a public function psModelEval, modified params
+  - pmSourceFitModel operates on a specified model
+  - pmSourceFitModel had option to apply/ignore PSF information
+  - pmSourceFitModel calculates yErr
+  - pmSourceFitModel calculates the covariance matrix
+  - added paramMask to pmSourceFitModel 
+  - pmSourceFitModel_EAM uses covar to carry in beta and param
+    limits...
+  - pmSourceFitModel requires solution to stay within image
+  - pmSourceFitModel calculates and saves chisq, nIter, nDOF
+  - pmSourceFitModel uses GaussNewtonDelta for frozen params
+  - added mask to pmSourceAdd, pmSourceSub
+  - model abstractions in pmSourceAdd, pmSourceSub
+  - fixed 'center' option in pmSourceAdd/Sub
+
+  - pmPSF params should be F64
+
+  lingering concerns:
+  - Polynomials and related functions are STILL messed up.  this is a
+    major priority!!!
+  - pmSourceFitModel calculates sqrt(var), psMinimize calculates sq(sqrt(var))
+  - does psImageAlloc zero the image?  if so, drop init routine
+    from pmSourcePSFClump
+  - pmSourceRoughClass uses RDNOISE, GAIN to calculate SN: should use
+    the source->noise image? part of pmSourceMoments?
+  - pmSourceMoments_EAM uses macro for checkRegion
+  - pmObjects_EAM.c uses some hardcoded mask values (LocalSky)
+  - why does LocalSky_EAM not need subImageMask->col0 = subImage->col0??
+  - psGetRowVectorFromImage should be in psLib (in psLibUtils)
+  - pmSourceSetPixelsCircle and pmSourceLocalSky probably could use
+    the psImageMaskRegion, etc functions.
+  - pmSourceSetPixelsCircle uses a hard-coded mask value
+  - define a mask registration process??
+  - pmSourceContour is using the model flux, not the image flux
+
+2005.09.05
+
+  I have a working version of PSPhot which handles PSF and galaxy
+  models.  I am tagging the module with an alpha version number. this
+  version compiles with psLib-0.5.0, at least my version of it.  My
+  current goal is to make it work with the current psLib head (nearly
+  0.7.0) with a mimimum of _EAM versions of functions.
+
+  the tag for the working version is dev_01
+
+2005.06.04
+
+  progress on psphot has moved along well.  At this point, the process
+  loads the image, finds the peaks, determines a best PSF model,
+  identifies objects which are consistent with that model, and
+  attempts to fit a galaxy model to the objects which are not
+  consistent with the model. 
+
+  a variety of minor issues remain, as well as some major issues. 
+
+  - use a proper noise image to keep the fits honest after other
+    objects have been subtracted.  
+
+  - define the masks in terms of a single mask image.  this image
+    could be provided initially by the user.   
+
+  - add noise enhancement (couple with mask marking subtraction)
+
+  - 
+
+2005.04.12
+
+- psPeak, psSource, etc: should be pmPeak, pmSource, etc
+- pmCullPeaks: should be pmCullImagePeaks
+
+- does pmSourceMoments need an image argument?
+
+- psLibInit / p_psTimeInit
+
+  we should not have to know about astronomy time concepts (ie, have a
+  timeConfig file) in order to use the time functions for basic work.
+  This means having a PS_TIME_UNIX which just works with the UNIX
+  clock and avoids all initialization issues.
+
+- psImageStats is much too slow for basic median
+  - this function does a complete sort on all pixel values.  we need
+  to use the histogram method (spec a different type of median)
+
+- psImageStats, ROBUST_MEDIAN fails
+- there are errors in psVectorStats for ROBUST MEAN, STDEV (& median?)
+  * this is ill-defined.  revise the ADD: too sensitive to the
+  binning, smoothing scales 
+
+- pmFindImagePeaks is not finding any peaks:
+  typo in interior row section: see bug 359
+  after this fix: PASS
+
+* p_psGetRowVectorFromImage : PASS
+* psGetRowVectorFromImage : PASS 
+- both have about the same time (~15 us +/- 5us) for 2048 pix on alala
+
+* pmFindVectorPeaks : PASS (returned row data in data.U32)
+
+- # include <string.h> : needed in psMetadata.c
+
+* MyListAddPeak / pmPeakAlloc are inconsistent wrt col,row vs x,y
+
+- why does psMetadataItemAllocV allocate a string of length
+  MAX_STRING_LENGTH for the comment, then use strncpy?
+
+  should be:
+
+    // set metadata item comment
+    if (comment == NULL) {
+        // Per SDRS, null isn't allowed, must use "" instead
+        metadataItem->comment = psStringCopy ("");
+    } else {
+        metadataItem->comment = psStringCopy (comment);
+    }
+
+psMetadataAdd -> psMetadataAddItem is all confused:
+ - we end up with two psMetadataItemAlloc calls, when only one should
+   be used
+ - psMetadataItemAlloc was doing the wrong thing if the incoming data
+   was NULL
+ - I think the logic in this block is wrong as well.  
+
+ * I fixed this in psMetadata.c
+
+2005.04.14 : 
+
+ - psImageStats is still much too slow
+
+ - pmObjects : changed to using psArray to carry peaks / sources,
+   rather than psList
+
+ - added function pmPeaksSubset to replace pmCullPeaks
+
+ - pmSourceLocalSky: added min/max fncs to force subimage to stay in
+   image
+
+ - Sx, Sy can go negative: have forced limit to 0,0
+
+ - pmSourceRoughClass: implemented sigmaX, sigmaY search for stellar
+   clump (uses pmComparePeakDescend)
+
+   - the search ignores the 0,0 pixel
+
+   - pmSourceRoughClass uses all detected peaks.  allow an exclusion
+     for peak > max, peak < min?
+
+   - need to push clump stats on the metadata
+
+   - the source classes are assigned so they are mutually exclusive 
+
+   - change PS_SOURCE_OTHER to STAR? 
+
+ - pmSourceSetPixelCircle:
+
+   - name should be pmSourceSetPixelsCircle (geometry could be an
+     option...)
+
+   - I cleaned the defined region to match the convensions of
+     pmSourceLocalSky
+
+   - truncate and force subimage to lie on image
+
+   - CheckRadius2 seems like an inefficient coding
+
+   - valid pixels have mask value of 1 (inverse of other funcs)
+
+ - pmSourceFitModel:
+
+   - num iterations is much too large
+
+   - 
+
+mrq:
+
+ - allocate alpha, beta arrays
+ - mrq2dcof (pars, alpha, beta)
+ - make guess pars', alpha', beta'
+ - mrq2dcof (pars', alpha', beta')
+ - if (chisq < old chisq), keep new guess
+
+
+2005.04.19:
+
+- psMinimizeLMChi2 has some serious problems.  I re-wrote it starting
+  from the mrqmin example in ohana.   after a variety little bugs, it
+  seems to work quite well.  the program 'modeltest.c' runs two
+  different gaussians through psMinimizeLMChi2, and gets the right
+  answer quite quickly.  The first pass at the implementation had a
+  variety of problems.  to avoid any worry about errors in the
+  psMatrix code, I implemented psGaussJordan, basing the code on the
+  elixir gaussj.c code.  this worked fine, and let me find the errors
+  elsewhere in the code. the LUD version now works just the same as
+  the GaussJ version.
+
+
+2005.04.21
+
+- psMinimizeLMChi2 does an excellent job now in most cases.  I am able
+  to run pmSourceFitModel on the PSF stars quite well.  I added in the
+  (nearly) correct errors (actually, just using sqrt(N), limiting to
+  N>=1).  I see one problem object, which is quite faint, and does not
+  converge well: I get very large values for x,y, Sx, Sy, etc.  I need
+  to think about constraining these fits from running way off scale.
+
+  fit:   0.123879 sec for 277 stars (gauss)
+  fit:   0.155940 sec for 277 stars (pgauss) (!)
+  fit:   0.153292 sec for 277 stars
+  (0.5 msec per star)
Index: /branches/eam_branch_20071023/psphot/doc/outline.txt
===================================================================
--- /branches/eam_branch_20071023/psphot/doc/outline.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/doc/outline.txt	(revision 15358)
@@ -0,0 +1,21 @@
+
+Here is a summary outline of the steps taken by psphotReadout:
+
+* setup (choose recipe, choose readout, define break-point)
+* create mask and weight images if needed, set mask for analysis region
+* psphotImageMedian : construct a background model image and subtract it
+* psphotFindPeaks   : smooth and find the peak pixels from the smoothed image
+  - no pmSource defined yet, only pmPeak
+* psphotSourceStats : create sources for each peak and measure their moments
+  - pmSource defined here, with pixels covering SKY_OUTER_RADIUS
+  - no pmModel is defined yet.
+* psphotBasicDeblend : identify blended sources by proximity and valley depth
+* psphotRoughClass : source classification guess based on moments
+  - set the source.type,mode.
+  - re-calculate moments for saturated stars
+* psphotChoosePSF : define the psf model
+  - this generates model fits to the identified psf stars
+    (one for each tested model)
+  - these models are not kept (seems like a waste, but later fits must be consistent)
+* psphotGuessModels : set the initial PSF model for each object (even EXTENDED)
+  - creates modelPSF entries
Index: /branches/eam_branch_20071023/psphot/doc/output.txt
===================================================================
--- /branches/eam_branch_20071023/psphot/doc/output.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/doc/output.txt	(revision 15358)
@@ -0,0 +1,15 @@
+
+we have several output formats:
+  - TEXT : separate ASCII tables for PSF, EXT, MNT, and NULL
+  - SX : sextractor-style ASCII file
+  - OBJ : dophot-style ASCII file
+  - CMP : old-style elixir format (header + text)
+  - CMF : fits-table
+
+we have several options for organization:
+  - single chip/readout : outroot.ext
+  - split chips : outroot.NN.ext
+  - mef chips : outroot.ext (extensions with names)
+
+  * only CMF will work with MEF model
+
Index: /branches/eam_branch_20071023/psphot/doc/psfmodel.txt
===================================================================
--- /branches/eam_branch_20071023/psphot/doc/psfmodel.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/doc/psfmodel.txt	(revision 15358)
@@ -0,0 +1,62 @@
+
+2007.09.25
+
+  The PSF model has parameters which vary across the image.  This 2D
+  is can be represented as either a polynomial (ordinary or chebychev)
+  or as an image map.  The user requests the maximum X & Y scale
+  (PSF.TREND.NX & PSF.TREND.NY) for the variations, and
+  psphotChoosePSF / pmPSFtry attempt to find the best choice for the
+  scale.  For the polynomial representations, NX and NY are the order
+  of the variation.  For the image map, NX and NY are the size of the
+  image map.
+
+2007.09.21
+
+  there are three places where we can choose to use errors in the fits or not:
+
+  * non-linear fitting of the models to the pixel flux distribution (poissonErrorsPhotLMM)
+  * linear fitting of the models to the pixel flux distribution (poissonErrorsPhotLin)
+  * fitting of the 2D variations in the psf parameters (poissonErrorsParams)
+  * fitting of the 2D variations in the aperture residuals
+
+2007.09.20
+
+  I am upgrading the PSF model to allow the parameter variation to be
+  modeled with pmTrend2D instead of just polynomials.  I am making a
+  list of places to modify the code:
+
+pmPSFAlloc : need a method beyong psfTrendMask to carry in the psf
+options
+
+pmPSF_ModelToFit : no need to change these
+
+update pmPSFBuildSimple to set the parameters of the pmTrend, which
+ever is used.
+
+pmPSFtry.c: some significant re-work!
+
+
+
+pmPSF_IO : need new functions to save / load the trend (psImages)
+
+2006.10.27
+
+  I have been working to fix the PSF modeling in psphot.  The PSF
+  model consists of a flux model for an individual object using an
+  analytical model with a number of parameters.  For a collection of
+  PSF objects, the variation of the parameters as a function of
+  position are then themselves fitted with a model.  The PSF model for
+  a single object consists of a radial profile with a functional form
+  f(z) where a given value of z defines an elliptical coutour of the
+  form z = \frac{x^2}{2\sigma_x^2} + \frac{y^2}{2\sigma_y^2} +
+  \sigma_{xy}xy.
+
+
+
+  The term \sigma_{xy} is difficult to model as a simple function of x
+  and y (eg, a low-order polynomial).  
+
+  A better model can be constructed for
+  \frac{\sigma_{xy}}{(\sigma_x^{-2} + \sigma_y^{-2})^2}, which varies
+  like a^2 \sin 2\theta 
+
Index: /branches/eam_branch_20071023/psphot/doc/psphot.txt
===================================================================
--- /branches/eam_branch_20071023/psphot/doc/psphot.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/doc/psphot.txt	(revision 15358)
@@ -0,0 +1,160 @@
+
+TBD priorities:
+
+- test psImageSmooth / remove psImageSmooth_EAM
+- unify pmSourceLocalSky / pmSourceLocalSky_EAM
+- unify pmSourceMoments_EAM / pmSourceMoments_EAM
+- merge with image data hierarchy stuff
+- better name for psPSF_Test
+
+Defined APIs:
+
+top-level psphot functions : these do not need to be in psModules
+  psMetadata  *psphotArguments ();
+  psImageData *psphotSetup ();
+  psStats     *psphotImageStats ();
+  psArray     *pmPeaksSigmaLimit ();
+  psArray     *psphotSourceStats ();
+  pmPSF       *psphotChoosePSF ();
+  bool         psphotApplyPSF ();
+  bool         psphotFitGalaxies ();
+  void         psphotOutput ();
+
+psphot-specific functions
+  bool         psphotMarkPSF ();
+  bool         psphotSubtractPSF ();
+  int          psphotSortBySN ();
+  int          psphotSaveImage ();
+
+** add the following entries to the psLibSDRS
+
+psLib extra utilities
+  bool         psTimerStart ();		- already in SDRS
+  void         psTimerFree (); 		- already in SDRS
+  bool         psTimerClear ();		- add to psLibSDRS
+  psF64        psTimerMark (); 		- already in SDRS
+  psS32        psLogArguments ();	- now: psArgumentVerbosity in SDRS
+  psS32        psTraceArguments ();	- now: psArgumentVerbosity in SDRS
+  int          psArgumentGet ();	- already in SDRS
+  int          psArgumentRemove ();	- already in SDRS
+  psVector    *psVectorCreate ();	- add to psLibSDRS
+  int          psImageCountPixelMask ();- add to psLibSDRS
+  psVector    *psGetRowVectorFromImage(); - add to psLibSDRS?
+  bool         p_psVectorPrintRow ();     - add to psLibSDRS?
+
+// basic image functions
+  bool         psImageInit ();		- already in SDRS
+  void         psImageSmooth_EAM ();    - test psLib version and drop
+
+// psLine functions
+  psLine      *psLineAlloc ();		- add to psLibSDRS
+  bool         psLineInit (); 		- add to psLibSDRS
+  bool         psLineAdd ();  		- add to psLibSDRS
+
+** compare with current implementation / update psLib
+   we need to define a mechanism to carry the beta values and the
+   parameter limits
+
+// minimize 
+  psBool       p_psMinLM_GuessABP_EAM ();
+  psBool       psMinimizeLMChi2_EAM();
+  psF64        p_psMinLM_dLinear ();  -- not included in pslib.h files
+
+** already in psLib : some need to be cleaned up by MHPCC
+
+// polynomial functions
+  psF64           Polynomial2DEval();
+  psImage        *psBuildSums2D();
+  psPolynomial2D *VectorFitPolynomial2DOrd_EAM();
+  psPolynomial2D *Polynomial2DAlloc();
+  void            psPolynomial2DDump ();
+  psPolynomial2D *RobustFit2D_nomask();
+  psPolynomial2D *RobustFit2D();
+  psVector       *Polynomial2DEvalVector();
+
+  psVector       *psBuildSums1D();
+  void            psPolynomial1DDump ();
+  psF64           Polynomial1DEval_EAM();
+  psVector       *Polynomial1DEvalVector_EAM();
+  psPolynomial1D *Polynomial1DAlloc();
+  psPolynomial1D *VectorFitPolynomial1DOrd_EAM();
+
+** make sure these are all in the psModules SDRS:
+
+pmObjects.h:
+  pmMoments           *pmMomentsAlloc();
+  pmModel             *pmModelAlloc();
+  pmSource            *pmSourceAlloc();
+  psVector            *pmFindVectorPeaks()
+  psArray             *pmFindImagePeaks()
+  psList              *pmCullPeaks()
+  pmSource            *pmSourceLocalSky()
+  bool                 pmSourceMoments()
+  pmPSFClump           pmSourcePSFClump()
+  bool                 pmSourceRoughClass()
+  bool                 pmSourceSetPixelsCircle()
+  pmModel             *pmSourceModelGuess()
+  psArray             *pmSourceContour()
+  bool                 pmSourceFitModel()
+  bool                 pmSourceAddModel()
+  bool                 pmSourceSubModel()
+  int                  pmModelParameterCount ();
+  char                *pmModelGetType ();
+  pmModelType          pmModelSetType ();
+  pmModelFunc          pmModelFunc_GetFunction ();
+  pmModelFlux          pmModelFlux_GetFunction ();
+  pmModelRadius        pmModelRadius_GetFunction ();
+  pmModelLimits        pmModelLimits_GetFunction ();
+  pmModelGuessFunc     pmModelGuessFunc_GetFunction ();
+  pmModelFromPSFFunc   pmModelFromPSFFunc_GetFunction ();
+  pmModelFitStatusFunc pmModelFitStatusFunc_GetFunction ();
+
+model function abstractions:
+  typedef psMinimizeLMChi2Func pmModelFunc;
+  typedef psF64 (*pmModelFlux)(const psVector *params);
+  typedef psF64 (*pmModelRadius)(const psVector *params, double flux);
+  typedef bool (*pmModelLimits)(psVector **beta_lim, psVector **params_min, psVector **params_max);
+  typedef bool (*pmModelGuessFunc)(pmModel *model, pmSource *source);
+  typedef bool (*pmModelFromPSFFunc)(pmModel *modelPSF, pmModel *modelFLT, pmPSF *psf);
+  typedef bool (*pmModelFitStatusFunc)(pmModel *model);
+
+conversions between elliptical shape representations
+  EllipseAxes  EllipseMomentsToAxes ();
+  EllipseShape EllipseAxesToShape ();
+  EllipseAxes  EllipseShapeToAxes ();
+
+output functions
+  bool         pmSourcesWriteText ();  	-- be careful about psImageData...
+  bool         pmSourcesWriteOBJ ();   	-- be careful about psImageData...
+  bool         pmSourcesWriteCMP ();   	-- be careful about psImageData...
+  bool         pmSourcesWriteCMF ();   	-- be careful about psImageData...
+  bool         pmSourcesWriteSX ();    	-- be careful about psImageData...
+  int          pmSourcesDophotType ();
+  bool         pmPeaksWriteText ();
+  bool         pmMomentsWriteText ();
+  bool         pmModelWritePSFs ();
+  bool         pmModelWriteFLTs ();
+  bool         pmModelWriteNULLs ();
+
+// psf utilities
+  pmPSF       *pmPSFAlloc ();
+  pmPSF_Test  *pmPSF_TestAlloc ();
+  pmPSF_Test  *pmPSF_TestModel ();
+  bool         pmPSFFromModels ();
+  pmModel     *pmModelFromPSF ();
+  bool         pmSourcePhotometry ();
+  bool         pmPSFMetricModel ();
+
+// psModule extra utilities
+  bool         pmSourceFitModel_EAM();  -- requires updated version of psMinimize...
+  bool         pmSourceLocalSky_EAM (); -- compare with pmSourceLocalSky & choose
+  bool         pmSourceMoments_EAM();   -- compare with pmSourceMoments & choose
+  bool         pmSourceDefinePixels();  -- be careful about psImageData...
+  bool         pmModelFitStatus ();
+  int          pmSourceDophotType ();
+  psF32        pmConfigLookupF32 ();	-- merge with Paul's image container hierarchy
+
+** this needs to be confronted with the phase2 / image container hierarchy
+
+// psImageData functions
+  psImageData *psImageDataAlloc ();
Index: /branches/eam_branch_20071023/psphot/doc/regions.txt
===================================================================
--- /branches/eam_branch_20071023/psphot/doc/regions.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/doc/regions.txt	(revision 15358)
@@ -0,0 +1,159 @@
+
+I am having some trouble with functions that refer to both subimages
+and regions.  there are inconsistencies between the coordinate
+conventions.  I am going to go through the list of image functions are
+decide if we can choose either 1) external coordinates always refer to
+parent coordinates or 2) something else!
+
+bool psImageSet(const psImage *image, int x, int y, double complex value);
+double complex psImageGet(const psImage *image, int x, int y);
+ - sdrs does not specify
+ - code uses subimage
+
+psRegion psRegionForImage(psImage *image, psRegion in);
+ - sdrs says subimage
+ - code uses subimage
+
+psImage *psFitsReadImage(psImage *out, const psFits *fits, psRegion region, int z);
+ - region here specifies a subimage of the disk image
+ 
+bool psFitsUpdateImage(psFits *fits, const psImage *input, int x0, int y0, int z);
+ - x0,y0 here refer to the disk image coordinates
+
+bool psFitsWriteImage(psFits *fits, psMetadata *header, const psImage
+*input, int depth, const char *extname);
+ - note that the output disk image is truncated to the subimage
+
+bool psFitsInsertImage(psFits *fits, psMetadata *header, const psImage *input, int depth,
+                       const char *extname, bool after);
+ - same as above
+
+psImage *psImageSubset(psImage *image, psRegion region);
+ - code uses region as coordinates in parent image
+ - sdrs does not specify
+ - XXX I think the range tests for region fail if the input is a
+   subimage
+
+psImage *psImageCopy(psImage *output, const psImage *input, psElemType type);
+ - maintains the col0,row0 values
+
+psVector *psImageRow(psVector *out, const psImage *input, int row);
+psVector *psImageCol(psVector *out, const psImage *input, int column);
+ - code uses subimage coordinates for row and column
+
+psVector *psImageSlice()
+psVector* psImageCut()
+ - code uses subimage coordinates (no correction)
+ - sdrs does not specify
+
+psImage *psImageRebin()
+ - mask and input image must currently agree
+
+psImage *psImageTransform()
+ - sdrs is unclear: I think the region defines the output image such
+ that the pixel in the input image *parent* coordinates which
+ corresponds to region.x0,y0 in turn corresponds to the output image
+ 0,0 pixel; the output image always has col0,row0 = 0,0
+
+long psImageCountPixelMask (psImage *mask, psRegion region, psMaskType value);
+ - sdrs says the region corresponds to subimage coordinates
+
+psPolynomial2D *psImageFitPolynomial(psPolynomial2D *coeffs, const psImage *input);
+psImage *psImageEvalPolynomial(psImage *input, const psPolynomial2D *coeffs);
+ - sdrs does not specify
+ - polynomial coordinates should refer to the *parent* image
+
+double complex psImagePixelInterpolate(input, x, y, ...)
+ - sdrs does not specify
+
+int psImageOverlaySection(psImage *image, const psImage *overlay, )
+ - sdrs does not specify
+
+psImage *psPixelsToMask()
+psPixels *psPixelsFromMask()
+ - sdrs does not specify
+
+void psImageMaskRegion()
+void psImageKeepRegion()
+ - sdrs does not specify
+
+void psImageMaskCircle()
+void psImageKeepCircle()
+ - sdrs does not specify
+
+pmReadout *pmSubtractSky(pmReadout *in, psPolynomial2D *poly, psImage *mask, psU8 maskVal, 
+                         int binFactor, psStats *stats, float clipSD);
+ - choice of polynomial coordinates
+
+stats *pmFringeStats (psArray *fringePoints, psImage *image, psMetadata *config);
+ - unspecified
+
+psF32 pmModelEval(pmModel *model, psImage *image, psS32 col, psS32 row);
+ - sdrs says col,row are in *subimage* coords
+
+psArray *pmFindImagePeaks(const psImage *image, float threshold);
+ - does not specify coordinate system
+ - code uses subimage?
+
+bool pmSourceDefinePixels()
+bool pmSourceRedefinePixels()
+ - sdrs does not specify very clearly
+
+
+
+---------
+
+functions which are not affected
+psFitsReadImage()
+psFitsUpdateImage()
+psFitsWriteImage()
+psFitsInsertImage()
+psImageCopy()  -- maintains col0,row0
+psImageRebin() -- mask and image must match
+
+functions which currently use parent coordinates (CORRECT)
+psImageSubset()  -- check on range tests
+psImageMaskRegion()
+psImageKeepRegion()
+psImageMaskCircle()
+psImageKeepCircle()
+pmSourceMoments -- (OK if input peaks are in parent coords)
+
+functions which currently use subimage coordinates (FIX)
+psImageSet()
+psImageGet()
+psRegionForImage() *fixed*
+psImageRow()
+psImageCol()
+psImageSlice()
+psImageCut()
+psImageCountPixelMask ();
+pmModelEval()
+pmFindImagePeaks() *fixed*
+pmSourceDefinePixels() *fixed by psRegionForImage fix*
+pmSourceRedefinePixels() *fixed by psRegionForImage fix*
+
+functions which are unclear (CHECK)
+psImageTransform()
+psImageFitPolynomial()
+psImageEvalPolynomial()
+psImagePixelInterpolate()
+psPixelsToMask()
+pmSubtractSky()
+pmFringeStats()
+
+
+***** one ambiguity: if we have a subimage, but we do not have the
+      parent image data (subimage does not carry dimensions of parent
+      array), then a 0 or negative upper-edge of a region cannot be
+      defined relative to the parent. we can specify it relative to
+      the subimage.  so, for example, given a subimage [32,100:32,100]
+      of a much larger image, then psRegionForImage with 0,0,0,0 as
+      input would return a region with values 32,100:32,100...
+
+
+
+
+
+*** things I've modified
+    psRegionForImage expects parent (not subimage) coordinates
Index: /branches/eam_branch_20071023/psphot/doc/speed.txt
===================================================================
--- /branches/eam_branch_20071023/psphot/doc/speed.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/doc/speed.txt	(revision 15358)
@@ -0,0 +1,129 @@
+
+psphot -g
+psModule -g
+psLib -O2
+on alala:
+
+alala: psphot -vv starfield.fits starfield starfield.cnf -resid starfield.resid.3.fits
+    load data: 0.983568 sec
+    System random seed value used  seed = 25904F7C7454AE25 hex
+    background: 1229.280029 +/- 27.700180
+    image stats: 0.063085 sec
+    System random seed value used  seed = 411E5A0600A49AF3 hex
+    fit background model: 0.580152 sec
+    smooth: 1.842415 sec
+    threshold: 83.100540 DN
+    4356 peaks: 0.571587 sec
+    4230 moments: 3.965099 sec
+    SN range: 3.355829 - 1043.401001
+    identified 107 blended objects (0.121737 sec)
+    selected candidate 300 PSF objects
+    fit flt:   3.682850 sec for 300 sources
+    fit psf:   1.704141 sec for 300 sources
+    fit stats: 0.000884 +/- 0.004499
+    try model PS_MODEL_QGAUSS, ap-fit: -0.007532 +/- 0.004499 : sky bias: -0.717999
+    select psf model: 5.402742 sec
+    selected psf model PS_MODEL_QGAUSS, ApResid: -0.007532 +/- 0.004499
+    fitting pixels with at least 27.700180 object counts
+    built models: 0.991345 (4230 objects)
+    built matrix: 1.472869 (27998 elements)
+    measure ensemble of PSFs: 4.334556
+    fit PSF models: 12.205738 sec for 4230 objects
+    replace unfitted models: 0.021473 sec (4230 objects)
+    measure aperture residuals : 0.451886 sec
+    measure full-frame aperture residual: 0.454032 sec
+    aperture residual: -0.002842 +/- 0.002729 : 0.166072 bias
+    wrote output: 4.648821 sec
+    complete psphot run: 35.334080 sec
+
+
+psphot -g
+psModule -O2
+psLib -O2
+on alala:
+
+alala: psphot -vv starfield.fits starfield starfield.cnf -resid starfield.resid.3.fits
+    load data: 1.001502 sec
+    System random seed value used  seed = 5F43A63F532D4B3F hex
+    background: 1229.280029 +/- 27.700180
+    image stats: 0.063169 sec
+    System random seed value used  seed = F5F5FF99A6578F61 hex
+    fit background model: 0.530384 sec
+    smooth: 1.856414 sec
+    threshold: 83.100540 DN
+    4365 peaks: 0.237292 sec
+    4239 moments: 4.087611 sec
+    SN range: 3.356469 - 1043.400879
+    identified 107 blended objects (0.123372 sec)
+    selected candidate 300 PSF objects
+    fit flt:   3.741444 sec for 300 sources
+    fit psf:   1.676478 sec for 300 sources
+    fit stats: 0.000883 +/- 0.004499
+    try model PS_MODEL_QGAUSS, ap-fit: -0.007532 +/- 0.004499 : sky bias: -0.717985
+    select psf model: 5.458156 sec
+    selected psf model PS_MODEL_QGAUSS, ApResid: -0.007532 +/- 0.004499
+    fitting pixels with at least 27.700180 object counts
+    built models: 0.791613 (4239 objects)
+    built matrix: 1.267909 (28101 elements)
+    measure ensemble of PSFs: 3.551364
+    fit PSF models: 11.240197 sec for 4239 objects
+    replace unfitted models: 0.018051 sec (4239 objects)
+    measure aperture residuals : 0.411856 sec
+    measure full-frame aperture residual: 0.413843 sec
+    aperture residual: -0.002831 +/- 0.002717 : 0.164772 bias
+    wrote output: 4.355429 sec
+    complete psphot run: 33.126283 sec
+
+after reducing indirection in pmSourceMoments:
+    4268 moments: 3.762452 sec
+
+psphot -O2
+psModule -O2
+psLib -O2
+
+alala: psphot -vv starfield.fits starfield starfield.cnf -resid starfield.resid.3.fits
+    load data: 0.484324 sec
+    System random seed value used  seed = 980050732A392EC7 hex
+    background: 1229.280029 +/- 27.700180
+    image stats: 0.058759 sec
+    System random seed value used  seed = 44E1A6EE77B0E9CE hex
+    fit background model: 0.395125 sec
+    smooth: 1.861776 sec
+    threshold: 83.100540 DN
+    4351 peaks: 0.234874 sec
+    4264 moments: 3.719435 sec
+    SN range: 2.023345 - 1043.401123
+    identified 124 blended objects (0.115230 sec)
+    selected candidate 300 PSF objects
+    fit flt:   3.525501 sec for 300 sources
+    fit psf:   1.595031 sec for 300 sources
+    fit stats: 0.000884 +/- 0.004499
+    try model PS_MODEL_QGAUSS, ap-fit: -0.007532 +/- 0.004499 : sky bias: -0.717976
+    select psf model: 5.160442 sec
+    selected psf model PS_MODEL_QGAUSS, ApResid: -0.007532 +/- 0.004499
+    fitting pixels with at least 27.700180 object counts
+    built models: 0.760805 (4264 objects)
+    built matrix: 0.908674 (23703 elements)
+    measure ensemble of PSFs: 3.115425
+    fit PSF models: 10.162397 sec for 4264 objects
+    replace unfitted models: 0.017491 sec (4264 objects)
+    measure aperture residuals : 0.408242 sec
+    measure full-frame aperture residual: 0.410207 sec
+    aperture residual: -0.002908 +/- 0.002734 : 0.170692 bias
+    wrote output: 4.225560 sec
+    complete psphot run: 30.149281 sec
+
+
+    fit PSF models: 10.162397 sec for 4264 objects
+    select psf model: 5.160442 sec
+    wrote output: 4.225560 sec
+    4264 moments: 3.719435 sec
+    measure ensemble of PSFs: 3.115425 sec
+
+    other : 3.5 sec
+
+    fixed per objects : 4 msec / object (17 of 30 sec)
+
+
+** still can re-compile with -DPS_NO_TRACE
+** ASSERTS cannot be compiled out at the moment!
Index: /branches/eam_branch_20071023/psphot/doc/versions.txt
===================================================================
--- /branches/eam_branch_20071023/psphot/doc/versions.txt	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/doc/versions.txt	(revision 15358)
@@ -0,0 +1,49 @@
+
+2006.02.02
+  I'm tagging psphot after I updated it to work with the readouts
+  using the structures being used for ppImage.  In this release, I
+  have also moved to autoconf for psphot, and the psphotReadout
+  portion is being defined as a library against which the other
+  elements are linked.  It should be possible to use this library with
+  ppImage, but I have not yet tested that.  
+
+  link psphot_dev_08 against psLib:eam_rel9_b2 and
+  psModule:eam_rel9_b2.
+
+2006.01.18
+  the tag below (psphot_dev_07) was moved to a later release.  It
+  should be used with psLib:eam_rel9_b1 from 2006/1/18 and
+  psModule:eam_rel9_p0.  
+
+2005.12.23
+  psphot_dev_07 should be compiled against psLib:eam_rel9_b1 and
+  psModule:eam_rel9_b1.  This version now handles the ensemble PSF
+  fitting, detection of blended sources, fitting of 'blends' (groups
+  of PSFs with distinct be 'blended' peaks) and 'blobs' (unresolved
+  extended objects, with a test for double PSF vs extended source
+  model.
+
+2005.11.25
+
+  psphot_dev_05 should be compiled against psLib tag eam_rel8_b2 and
+  psModules tag eam_rel8_b1.  this version removes all _EAM versions
+  of psLib and psModules code.  It now uses the psModules version of
+  the object code.
+
+  psphot_dev_04 should be compiled against psLib tag eam_rel8_b2 this
+  version removes all _EAM versions of psLib code, so that it is now
+  consistent with psLib.v8 (fixes are pushed into the eam_rel8_b2
+  branch).
+
+2005.11.16
+
+  psphot_dev_03 should be compiled against psLib tag eam_rel8_b1, a
+  branch starting from psLib rel8_0.  I needed to make some minor
+  fixes in psLib to work successfully with psLib rel8_0.
+
+  psphot_dev_02 was getting segfaults from a bug added into psLib in
+  rel8_0 in p_psVectorClippedStats.  I also fixed some errors arising
+  from changing types (argument to psMetadataParseConfig).
+
+
+
Index: /branches/eam_branch_20071023/psphot/psphot-config.in
===================================================================
--- /branches/eam_branch_20071023/psphot/psphot-config.in	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/psphot-config.in	(revision 15358)
@@ -0,0 +1,87 @@
+#! /bin/sh
+
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+top_srcdir=@ABS_SRCDIR@
+
+usage()
+{
+    cat <<EOF
+Usage: psphot-config [OPTION]
+
+Known values for OPTION are:
+
+  --prefix		print psphot installation prefix
+  --libs		print library linking information
+  --cflags		print pre-processor and compiler flags
+  --build-libs		print library linking information to the build (non-installed) version
+  --build-cflags	print pre-processor and compiler flags to the build (non-installed) version
+  --help		display this help and exit
+  --version		output version information
+
+EOF
+
+    exit $1
+}
+
+if test $# -eq 0; then
+    usage 1
+fi
+
+cflags=false
+libs=false
+
+while test $# -gt 0; do
+    case "$1" in
+    -*=*) optarg=`echo "$1" | sed 's/[-_a-zA-Z0-9]*=//'` ;;
+    *) optarg= ;;
+    esac
+
+    case "$1" in
+    --prefix=*)
+	prefix=$optarg
+	;;
+
+    --prefix)
+	echo $prefix
+	;;
+
+    --version)
+	echo @VERSION@
+	exit 0
+	;;
+
+    --help)
+	usage 0
+	;;
+
+    --cflags)
+       	echo -I${includedir} @PSPHOT_CFLAGS@
+       	;;
+
+    --libs)
+       	echo -L${libdir} -lpsphot @PSPHOT_LIBS@
+       	;;
+
+    --build-cflags)
+       	echo @SRCINC@ @PSPHOT_CFLAGS@
+       	;;
+
+    --build-libs)
+       	echo -L@ABS_SRCDIR@/src/.libs -lpsphot @PSPHOT_LIBS@
+       	;;
+
+    --deps)
+       	echo @PSPHOT_LIBS@
+       	;;
+    *)
+	usage
+	exit 1
+	;;
+    esac
+    shift
+done
+
+exit 0
Index: /branches/eam_branch_20071023/psphot/psphot.pc.in
===================================================================
--- /branches/eam_branch_20071023/psphot/psphot.pc.in	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/psphot.pc.in	(revision 15358)
@@ -0,0 +1,12 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: libpsphot
+Description: Pan-STARRS Photometry Library
+Version: @VERSION@
+Requires: pslib psmodules
+Libs: -L${libdir} -lpsphot
+Libs.private: @PSPHOT_LIBS@
+Cflags: -I${includedir}
Index: /branches/eam_branch_20071023/psphot/src/.cvsignore
===================================================================
--- /branches/eam_branch_20071023/psphot/src/.cvsignore	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/.cvsignore	(revision 15358)
@@ -0,0 +1,18 @@
+*.o
+*.lo
+.libs
+.deps
+Makefile
+Makefile.in
+libpsphot.la
+psphot
+psphotTest
+psphot.loT
+psphotErrorCodes.h
+psphotErrorCodes.c
+config.h
+config.h.in
+stamp-h1
+
+polyfitTest
+growthTest
Index: /branches/eam_branch_20071023/psphot/src/Makefile.am
===================================================================
--- /branches/eam_branch_20071023/psphot/src/Makefile.am	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/Makefile.am	(revision 15358)
@@ -0,0 +1,100 @@
+
+lib_LTLIBRARIES = libpsphot.la
+libpsphot_la_CPPFLAGS = $(PSLIB_CFLAGS) $(PSMODULE_CFLAGS) $(PSPHOT_CFLAGS)
+
+bin_PROGRAMS = psphot
+
+psphot_CPPFLAGS = $(PSLIB_CFLAGS) $(PSMODULE_CFLAGS) $(PSPHOT_CFLAGS)
+psphot_LDFLAGS = $(PSLIB_LIBS) $(PSMODULE_LIBS) $(PSPHOT_LIBS)
+psphot_LDADD = libpsphot.la
+
+psphot_SOURCES = \
+        psphot.c                \
+	psphotArguments.c	\
+	psphotCleanup.c	   	\
+	psphotImageLoop.c	\
+	psphotMosaicChip.c	\
+	psphotParseCamera.c
+
+libpsphot_la_SOURCES = \
+	psphotErrorCodes.c	 \
+	pmFootprint.c		 \
+	psphotVersion.c		 \
+	psphotModelGroupInit.c	 \
+	psphotMaskReadout.c	 \
+	psphotDefineFiles.c	 \
+	psphotReadout.c		 \
+	psphotImageMedian.c	 \
+	psphotFindPeaks.c	 \
+	psphotSourceStats.c	 \
+	psphotRoughClass.c	 \
+	psphotBasicDeblend.c	 \
+	psphotChoosePSF.c	 \
+	psphotGuessModels.c      \
+	psphotFitSourcesLinear.c \
+	psphotBlendFit.c	 \
+	psphotReplaceUnfit.c	 \
+	psphotApResid.c		 \
+	psphotMagnitudes.c	 \
+	psphotSkyReplace.c	 \
+	psphotEvalPSF.c		 \
+	psphotEvalFLT.c		 \
+	psphotSourceFits.c	 \
+	psphotRadiusChecks.c	 \
+	psphotOutput.c		 \
+	psphotGrowthCurve.c	 \
+	psphotFakeSources.c	 \
+	psphotModelWithPSF.c     \
+	psphotExtendedSources.c	 \
+	psphotPetrosian.c	 \
+	psphotIsophotal.c	 \
+	psphotAnnuli.c		 \
+	psphotKron.c		 \
+	psphotKernelFromPSF.c	 \
+	psphotPSFConvModel.c	 \
+	psphotModelTest.c	 \
+	psphotFitSet.c		 \
+	psphotSourceFreePixels.c \
+	psphotSummaryPlots.c     \
+	psphotMergeSources.c	 \
+	psphotLoadPSF.c	         \
+	psphotReadoutCleanup.c	 \
+	psphotSourcePlots.c	 \
+	psphotRadialPlot.c	 \
+	psphotDeblendSatstars.c	 \
+	psphotMosaicSubimage.c	 \
+	psphotMakeResiduals.c	 \
+	psphotSourceSize.c	 \
+	psphotDiagnosticPlots.c	 \
+	psphotMakeFluxScale.c	 \
+	psphotCheckStarDistribution.c \
+	psphotAddNoise.c
+
+include_HEADERS = \
+	psphot.h \
+	pmFootprint.h \
+	psphotErrorCodes.h
+
+noinst_HEADERS = \
+	psphotInternal.h \
+	psphotStandAlone.h
+
+clean-local:
+	-rm -f TAGS
+
+# Tags for emacs
+tags:
+	etags `find . -name \*.[ch] -print`
+
+# Error codes.
+BUILT_SOURCES = psphotErrorCodes.h psphotErrorCodes.c
+CLEANFILES = psphotErrorCodes.h psphotErrorCodes.c
+EXTRA_DIST = psphotErrorCodes.dat psphotErrorCodes.c.in psphotErrorCodes.h.in \
+	models/pmModel_STRAIL.c \
+	models/pmModel_TEST1.c
+
+psphotErrorCodes.h : psphotErrorCodes.dat psphotErrorCodes.h.in
+	$(ERRORCODES) --data=psphotErrorCodes.dat --outdir=. psphotErrorCodes.h
+
+psphotErrorCodes.c : psphotErrorCodes.dat psphotErrorCodes.c.in psphotErrorCodes.h
+	$(ERRORCODES) --data=psphotErrorCodes.dat --outdir=. psphotErrorCodes.c
Index: /branches/eam_branch_20071023/psphot/src/models/pmModel_STRAIL.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/models/pmModel_STRAIL.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/models/pmModel_STRAIL.c	(revision 15358)
@@ -0,0 +1,618 @@
+ 
+/****************************************************************************** 
+    params->data.F32[0] = So; 
+    params->data.F32[1] = Zo; 
+    params->data.F32[2] = Xo; 
+    params->data.F32[3] = Yo; 
+    params->data.F32[4] = 1 / SigmaX; 
+    params->data.F32[5] = 1 / SigmaY; 
+    params->data.F32[6] = Sxy; 
+    params->data.F32[7] = length; 
+    params->data.F32[8] = theta; 
+*****************************************************************************/ 
+ 
+# define PM_MODEL_FUNC       	  pmModelFunc_STRAIL
+# define PM_MODEL_FLUX       	  pmModelFlux_STRAIL
+# define PM_MODEL_GUESS      	  pmModelGuess_STRAIL
+# define PM_MODEL_LIMITS     	  pmModelLimits_STRAIL
+# define PM_MODEL_RADIUS     	  pmModelRadius_STRAIL
+# define PM_MODEL_FROM_PSF   	  pmModelFromPSF_STRAIL
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_STRAIL
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_STRAIL
+
+psF32 PM_MODEL_FUNC(psVector *deriv, 
+			 const psVector *params, 
+			 const psVector *x) 
+{ 
+    psF32 *PAR = params->data.F32; 
+ 
+    psF32 trailLength = PAR[7]; 
+    psF32 theta = PAR[8]; 
+ 
+    psF32 x0 = PAR[2];  //streak center 
+    psF32 y0 = PAR[3];  //streak center 
+ 
+    //S values (1/sigma for x and y case, sigma for xy case) 
+    psF32 sx = PAR[4];   
+    psF32 sy = PAR[5];   
+    psF32 sxy = PAR[6];  
+ 
+    psF32 sinT=sin(theta); 
+    psF32 cosT=cos(theta); 
+    psF32 sin2T=sin(2.0*theta); 
+    psF32 cos2T=cos(2.0*theta); 
+ 
+    //    printf("Trying object at %4.1f,%4.1f with length %3.1f and angle %1.3f\r", x0, y0, length, theta); 
+ 
+    //current location relative to trail center 
+    psF32 X  = x->data.F32[0] - x0; 
+    psF32 Y  = x->data.F32[1] - y0; 
+ 
+    //x' and y' location (trail-orienter coords) 
+    psF32 xs = X*cosT + Y*sinT; 
+    psF32 ys = -1.0*X*sinT + Y*cosT; 
+ 
+    //initialize variables to be changed below 
+    psF32 x1 = 0; 
+    psF32 y1 = 0; 
+    psF32 px = 0; 
+    psF32 py = 0; 
+    psF32 z  = 0; 
+    psF32 zx = 0; 
+    psF32 t  = 0; 
+    psF32 tx = 0; 
+    psF32 r  = 0; 
+    psF32 rx = 0; 
+    psF32 f  = 0; 
+ 
+    psF32 sxrot = 0; 
+    psF32 syrot = 0; 
+    psF32 sxyrot = 0; 
+    psF32 dsxrot = 0; 
+    psF32 dsyrot = 0; 
+    psF32 dsxyrot = 0; 
+ 
+    //    psF32 Rx = 0; 
+    //    psF32 Ry = 0; 
+    //    psF32 Rxy = 0; 
+ 
+ 
+    //calculate new S values (1/sigma) for rotated frame 
+    psF32 sxrotsq = PS_SQR(cosT*sx) + PS_SQR(sinT*sy) + cosT*sinT*sxy; 
+    psF32 syrotsq = PS_SQR(cosT*sy) + PS_SQR(sinT*sx) - cosT*sinT*sxy; 
+ 
+    //    psF32 testtwo=10.1; 
+    //    psF32 testone=fabsf(testtwo); 
+    //    fprintf (stderr, "Test: %f is the absolute value of %f?\n",testone,testtwo); 
+    if (sxrotsq<0) { 
+      sxrot = sqrt(-(sxrotsq)); 
+      syrot = sqrt(syrotsq); 
+      fprintf (stderr, "error in sxrotsq: Neg,  sxrotsq=%f sx=%f sy=%f sxy=%f theta=%f\n",sxrotsq,sx,sy,sxy,theta); 
+    } else if (syrotsq<0) { 
+      sxrot = sqrt(sxrotsq); 
+      syrot = sqrt(-(syrotsq)); 
+      fprintf (stderr, "error in syrotsq: Neg,  syrotsq=%f sx=%f sy=%f sxy=%f theta=%f\n",syrotsq,sx,sy,sxy,theta); 
+    } else if (sxrotsq==0){ 
+      sxrot = 0.01; 
+      syrot = sqrt(syrotsq); 
+      fprintf (stderr, "error in sxrotsq: Zero,  sxrotsq=%f \n",sxrotsq); 
+    } else if (syrotsq==0) { 
+      syrot = 0.01; 
+      sxrot = sqrt(sxrotsq); 
+      fprintf (stderr, "error in syrotsq: Zero,  syrotsq=%f \n",syrotsq); 
+      //      return(0); 
+    }else { 
+      sxrot = sqrt(sxrotsq); 
+      syrot = sqrt(syrotsq); 
+    } 
+ 
+    sxyrot = sxy*cos2T + (PS_SQR(sy) - PS_SQR(sx))*sin2T; 
+ 
+      if (isnan(sxrot)) { 
+	fprintf (stderr, "error in sxrot  syrot=%f sx=%f sy=%f sxy=%f cosT=%f sinT=%f\n",syrot,sx,sy,sxy,cosT,sinT); 
+      } else if (isnan(syrot)) { 
+	fprintf (stderr, "error in syrot  sxrot=%f sx=%f sy=%f sxy=%f cosT=%f sinT=%f\n",sxrot,sx,sy,sxy,cosT,sinT); 
+      }  
+ 
+    //calculate length of pipe (not of trail motion) 
+    psF32 length = trailLength - 2.0*2.0/sxrot; 
+ 
+ 
+    if (xs < length/(-2.0)) { 
+      x1 = (xs+length/2.0)*cosT - ys*sinT; //Endpoint1 
+      y1 = (xs+length/2.0)*sinT + ys*cosT; //Endpoint1 
+      //1.6 factor comes from by-eye testing of fits, sqrt from the later squaring of it 
+      //1.6 ~= phi (golden mean)...coincidence? 
+      px = sxrot*x1/sqrt(1.6); 
+      py = syrot*y1; 
+ 
+      //first find out what the falloff in the x direction is... 
+      zx  = 0.5*PS_SQR(px); 
+      tx = 1 + zx + zx*zx/2.0 + zx*zx*zx/6.0; 
+      rx = 1.0 / (tx + zx*zx*zx*zx/24.0); 
+ 
+      //...and now in the y direction 
+      z  = 0.5*PS_SQR(py)+sxyrot*x1*y1; 
+      t  = 1 + z + z*z/2.0; 
+      r  = 1.0 / (t + z*z*z/6.0); /* exp (-Z) */ 
+      f  = PAR[1]*rx*r + PAR[0]; 
+ 
+    } else if (xs > length/2.0){ 
+      x1 = (xs-length/2.0)*cosT - ys*sinT; //Endpoint2 
+      y1 = (xs-length/2.0)*sinT + ys*cosT; //Endpoint2 
+      px = sxrot*x1/sqrt(1.6); 
+      py = syrot*y1; 
+      zx  = 0.5*PS_SQR(px); 
+      tx = 1 + zx + zx*zx/2.0 + zx*zx*zx/6.0; 
+      rx = 1.0 / (tx + zx*zx*zx*zx/24.0); 
+ 
+      z  = 0.5*PS_SQR(py)+sxyrot*x1*y1; 
+      t  = 1 + z + z*z/2.0; 
+      r  = 1.0 / (t + z*z*z/6.0); /* exp (-Z) */ 
+      f  = PAR[1]*rx*r + PAR[0]; 
+ 
+      if (isnan(r)) { 
+	fprintf (stderr, "error in +r  t=%f z=%f\n",t,z); 
+      }  
+ 
+    } else { 
+      x1 = -ys*sinT; 
+      y1 = ys*cosT;  
+      px = sx*x1; 
+      py = sy*y1; 
+      z  = 0.5*PS_SQR(px) + 0.5*PS_SQR(py) + sxy*x1*y1; 
+      t  = 1 + z + z*z/2.0; 
+      r  = 1.0 / (t + z*z*z/6.0); /* exp (-Z) */ 
+      rx = 1.0;  //so that dF/dF0 can be generalized 
+      f  = PAR[1]*r + PAR[0]; 
+ 
+      if (isnan(r)) { 
+	fprintf (stderr, "error in midr  t=%f z=%f\n",t,z); 
+      }  
+    } 
+ 
+     
+    //ok...so this is df/dPAR[X] 
+    if (deriv != NULL) { 
+        psF32 q = 1; 
+	//stable 
+        deriv->data.F32[0] = +1.0; 
+        deriv->data.F32[1] = +r * rx; 
+ 
+	  if (isnan(deriv->data.F32[0])) { 
+	    fprintf (stderr, "error in deriv0\n"); 
+	  } else if (isnan(deriv->data.F32[1])) { 
+	    fprintf (stderr, "error in deriv1 r=%f rx=%f\n",r,rx); 
+	  }  
+ 
+	dsxrot = 2.0*PS_SQR(sy)*sinT*cosT - 2.0*PS_SQR(sx)*sinT*cosT + sxy*(PS_SQR(cosT)-PS_SQR(sinT)); 
+	dsyrot = 2.0*PS_SQR(sx)*sinT*cosT - 2.0*PS_SQR(sy)*sinT*cosT - sxy*(PS_SQR(cosT)-PS_SQR(sinT)); 
+	dsxyrot = 2.0*cos2T*(PS_SQR(sy)-PS_SQR(sx)) - 2.0*sxy*sin2T; 
+ 
+	  if (isnan(dsxrot)) { 
+	    fprintf (stderr, "error in dsxrot\n"); 
+	  } else if (isnan(dsyrot)) { 
+	    fprintf (stderr, "error in dsyrot\n"); 
+	  } else if (isnan(dsxyrot)) { 
+	    fprintf (stderr, "error in dsxyrot\n"); 
+	  }  
+	 
+	//initialize variables definied in the if statements 
+	//	psF32 XXX=0; 
+	//	psF32 YYY=0; 
+ 
+	   
+	// variable over piecewise func 
+	// change the endcaps to be 4th order gaussians with sxrot_fit=1.6*sxrot 
+	// y' direction can ge adequately modelled by a 3rd order gaussian with syrot 
+	if (xs < length/(-2.0)) { 
+	   
+	  q=PAR[1]*r*rx; 
+	  deriv->data.F32[2] = -q*(rx*tx/1.6*x1*PS_SQR(sxrot) + r*t*y1*sxyrot); 
+	  deriv->data.F32[3] = -q*r*t*(y1*PS_SQR(syrot) + x1*sxyrot); 
+	  deriv->data.F32[4] = -q*(rx*tx*x1*sx*PS_SQR(cosT)/1.6*(x1+2.0*cosT/sxrot) + r*t*y1*sx*sinT*(y1*sinT+2.0*PS_SQR(syrot)*PS_SQR(cosT)/(sxrot*sxrot*sxrot)) + 2.0*r*t*sx*(-1.0*x1*y1*sin2T+y1*sxyrot*(cosT*cosT*cosT)/(sxrot*sxrot*sxrot)+x1*sxyrot*(sinT*cosT*cosT)/(sxrot*sxrot*sxrot))); 
+	  deriv->data.F32[5] = -q*(rx*tx*x1*sy*PS_SQR(sinT)/1.6*(x1+2.0*cosT/sxrot) + r*t*y1*sy*(y1*PS_SQR(cosT)+2.0*PS_SQR(syrot)*(sinT*sinT*sinT)/(sxrot*sxrot*sxrot)) + 2.0*r*t*sy*(x1*y1*sin2T+y1*sxyrot*(sinT*sinT*cosT)/(sxrot*sxrot*sxrot)+x1*sxyrot*(sinT*sinT*sinT)/(sxrot*sxrot*sxrot))); 
+	  deriv->data.F32[6] = -q*(rx*tx*x1*cosT*sinT/3.2*(x1+2.0*cosT/sxrot) + r*t*y1*cosT*sinT/2.0*(-y1+2.0*PS_SQR(syrot)*sinT/(sxrot*sxrot*sxrot)) + r*t*(x1*y1*cos2T+y1*sxyrot*sinT*cosT*cosT/(sxrot*sxrot*sxrot)+x1*sxyrot*sinT*sinT*cosT/(sxrot*sxrot*sxrot))); 
+	  deriv->data.F32[7] = -q*(rx*tx*PS_SQR(sxrot)*x1*cosT/3.2 + r*t*PS_SQR(syrot)*y1*sinT/2.0 + r*t*sxyrot/2.0*(y1*cosT+x1*sinT)); 
+	  deriv->data.F32[8] = -q*(rx*tx*x1/3.2*(x1*(2.0*sinT*cosT*(PS_SQR(sy)-PS_SQR(sx))+sxy*(PS_SQR(cosT)-PS_SQR(sinT)))+PS_SQR(sxrot)*(2.0*cosT*(2.0*sinT*cosT*(PS_SQR(sy)-PS_SQR(sx))+sxy*(PS_SQR(cosT)-PS_SQR(sinT)))/(sxrot*sxrot*sxrot) - sinT*length)) + r*t*y1/2.0*(y1*(2.0*sinT*cosT*(PS_SQR(sx)-PS_SQR(sy))-sxy*(PS_SQR(cosT)-PS_SQR(sinT)))+PS_SQR(syrot)*(2.0*sinT*(2.0*sinT*cosT*(PS_SQR(sy)-PS_SQR(sx))+sxy*(PS_SQR(cosT)-PS_SQR(sinT)))/(sxrot*sxrot*sxrot)+cosT*length)) + r*t*(2.0*x1*y1*(cos2T*(PS_SQR(sy)-PS_SQR(sx))-sxy*sin2T)+y1*sxyrot/2.0*(2.0*cosT*(2.0*sinT*cosT*(PS_SQR(sy)-PS_SQR(sx))+sxy*(PS_SQR(cosT)-PS_SQR(sinT)))/(sxrot*sxrot*sxrot)-length*sinT)+x1*sxyrot/2.0*(2.0*sinT*(2.0*sinT*cosT*(PS_SQR(sy)-PS_SQR(sx))+sxy*(PS_SQR(cosT)-PS_SQR(sinT)))/(sxrot*sxrot*sxrot)+length*cosT))); 
+ 
+	  /*	  if (isnan(deriv->data.F32[2])) { 
+	    fprintf (stderr, "error in deriv2\n"); 
+	  } else if (isnan(deriv->data.F32[3])) { 
+	    fprintf (stderr, "error in deriv3\n"); 
+	  } else if (isnan(deriv->data.F32[4])) { 
+	    fprintf (stderr, "error in deriv4\n"); 
+	  } else if (isnan(deriv->data.F32[5])) { 
+	    fprintf (stderr, "error in deriv5\n"); 
+	  } else if (isnan(deriv->data.F32[6])) { 
+	    fprintf (stderr, "error in deriv6\n"); 
+	  } else if (isnan(deriv->data.F32[7])) { 
+	    fprintf (stderr, "error in deriv7\n"); 
+	  } else if (isnan(deriv->data.F32[8])) { 
+	    fprintf (stderr, "error in deriv8\n"); 
+	  } 
+	  */ 
+	     
+ 
+ 
+	} else if (xs > length/2.0){ 
+ 
+	  q=PAR[1]*r*rx; 
+	  deriv->data.F32[2] = -q*(rx*tx/1.6*x1*PS_SQR(sxrot) + r*t*y1*sxyrot); 
+	  deriv->data.F32[3] = -q*r*t*(y1*PS_SQR(syrot) + x1*sxyrot); 
+	  deriv->data.F32[4] = -q*(rx*tx*x1*sx*PS_SQR(cosT)/1.6*(x1-2.0*cosT/sxrot) + r*t*y1*sx*sinT*(y1*sinT-2.0*PS_SQR(syrot)*PS_SQR(cosT)/(sxrot*sxrot*sxrot)) + 2.0*r*t*sx*(-1.0*x1*y1*sin2T-y1*sxyrot*(cosT*cosT*cosT)/(sxrot*sxrot*sxrot)-x1*sxyrot*(sinT*cosT*cosT)/(sxrot*sxrot*sxrot))); 
+	  deriv->data.F32[5] = -q*(rx*tx*x1*sy*PS_SQR(sinT)/1.6*(x1-2.0*cosT/sxrot) + r*t*y1*sy*(y1*PS_SQR(cosT)-2.0*PS_SQR(syrot)*(sinT*sinT*sinT)/(sxrot*sxrot*sxrot)) + 2.0*r*t*sy*(x1*y1*sin2T-y1*sxyrot*(sinT*sinT*cosT)/(sxrot*sxrot*sxrot)-x1*sxyrot*(sinT*sinT*sinT)/(sxrot*sxrot*sxrot))); 
+	  deriv->data.F32[6] = -q*(rx*tx*x1*cosT*sinT/3.2*(x1-2.0*cosT/sxrot) + r*t*y1*cosT*sinT/2.0*(-y1-2.0*PS_SQR(syrot)*sinT/(sxrot*sxrot*sxrot)) + r*t*(x1*y1*cos2T-y1*sxyrot*sinT*cosT*cosT/(sxrot*sxrot*sxrot)-x1*sxyrot*sinT*sinT*cosT/(sxrot*sxrot*sxrot))); 
+	  deriv->data.F32[7] = q*(rx*tx*PS_SQR(sxrot)*x1*cosT/3.2 + r*t*PS_SQR(syrot)*y1*sinT/2.0 + r*t*sxyrot/2.0*(y1*cosT+x1*sinT)); 
+	  deriv->data.F32[8] = -q*(rx*tx*x1/3.2*(x1*(2.0*sinT*cosT*(PS_SQR(sy)-PS_SQR(sx))+sxy*(PS_SQR(cosT)-PS_SQR(sinT)))-PS_SQR(sxrot)*(2.0*cosT*(2.0*sinT*cosT*(PS_SQR(sy)-PS_SQR(sx))+sxy*(PS_SQR(cosT)-PS_SQR(sinT)))/(sxrot*sxrot*sxrot) - sinT*length)) + r*t*y1/2.0*(y1*(2.0*sinT*cosT*(PS_SQR(sx)-PS_SQR(sy))-sxy*(PS_SQR(cosT)-PS_SQR(sinT)))-PS_SQR(syrot)*(2.0*sinT*(2.0*sinT*cosT*(PS_SQR(sy)-PS_SQR(sx))+sxy*(PS_SQR(cosT)-PS_SQR(sinT)))/(sxrot*sxrot*sxrot)+cosT*length)) + r*t*(2.0*x1*y1*(cos2T*(PS_SQR(sy)-PS_SQR(sx))-sxy*sin2T)-y1*sxyrot/2.0*(2.0*cosT*(2.0*sinT*cosT*(PS_SQR(sy)-PS_SQR(sx))+sxy*(PS_SQR(cosT)-PS_SQR(sinT)))/(sxrot*sxrot*sxrot)-length*sinT)-x1*sxyrot/2.0*(2.0*sinT*(2.0*sinT*cosT*(PS_SQR(sy)-PS_SQR(sx))+sxy*(PS_SQR(cosT)-PS_SQR(sinT)))/(sxrot*sxrot*sxrot)+length*cosT))); 
+ 
+ 
+	  /*	  if (isnan(deriv->data.F32[2])) { 
+	    fprintf (stderr, "error in deriv2\n"); 
+	  } else if (isnan(deriv->data.F32[3])) { 
+	    fprintf (stderr, "error in deriv3\n"); 
+	  } else if (isnan(deriv->data.F32[4])) { 
+	    fprintf (stderr, "error in deriv4\n"); 
+	  } else if (isnan(deriv->data.F32[5])) { 
+	    fprintf (stderr, "error in deriv5\n"); 
+	  } else if (isnan(deriv->data.F32[6])) { 
+	    fprintf (stderr, "error in deriv6\n"); 
+	  } else if (isnan(deriv->data.F32[7])) { 
+	    fprintf (stderr, "error in deriv7\n"); 
+	  } else if (isnan(deriv->data.F32[8])) { 
+	    fprintf (stderr, "error in deriv8\n"); 
+	  } 
+	  */ 
+ 
+	} else { 
+	  // this does not change from before, as the y' falloff can be modelled by the standard 3rd order gaussian  
+	  // note difference from a pure gaussian: q = PAR[1]*r 
+	  q = PAR[1]*r*r*t; 
+	  deriv->data.F32[2] = q*(PS_SQR(sx*sinT)*x1 - PS_SQR(sy)*y1*cosT*sinT - sxy*x1*sinT*cosT + sxy*y1*PS_SQR(sinT)); 
+	  deriv->data.F32[3] = q*(-1*PS_SQR(sx)*x1*sinT*cosT + PS_SQR(sy*cosT)*y1 + sxy*x1*PS_SQR(cosT) - sxy*y1*sinT*cosT); 
+	  deriv->data.F32[4] = -q*sx*PS_SQR(x1); 
+	  deriv->data.F32[5] = -q*sy*PS_SQR(y1); 
+	  deriv->data.F32[6] = -q*x1*y1; 
+	  deriv->data.F32[7] = 0; 
+	  deriv->data.F32[8] = -q*( PS_SQR(sx)*x1*(xs*sinT - ys*cosT) + PS_SQR(sy)*y1*(xs*cosT - ys*sinT) + sxy*x1*(xs*cosT - ys*sinT) + sxy*y1*(xs*sinT - ys*cosT) ); 
+ 
+	  if (isnan(deriv->data.F32[2])) { 
+	    fprintf (stderr, "error in deriv2\n"); 
+	  } else if (isnan(deriv->data.F32[3])) { 
+	    fprintf (stderr, "error in deriv3\n"); 
+	  } else if (isnan(deriv->data.F32[4])) { 
+	    fprintf (stderr, "error in deriv4\n"); 
+	  } else if (isnan(deriv->data.F32[5])) { 
+	    fprintf (stderr, "error in deriv5\n"); 
+	  } else if (isnan(deriv->data.F32[6])) { 
+	    fprintf (stderr, "error in deriv6\n"); 
+	  } else if (isnan(deriv->data.F32[7])) { 
+	    fprintf (stderr, "error in deriv7\n"); 
+	  } else if (isnan(deriv->data.F32[8])) { 
+	    fprintf (stderr, "error in deriv8\n"); 
+	  } 
+ 
+ 
+	} 
+    } 
+    return(f); 
+} 
+ 
+//fixed  
+// XXX this needs to apply the axis ratio limits to prevent avoid solutions
+# define AR_MAX 20.0
+# define AR_RATIO 0.99
+bool PM_MODEL_LIMITS (psMinConstraintMode mode, int nParam, float *params, float *beta) {
+ 
+    float beta_lim = 0;
+    float params_min = 0;
+    float params_max = 0;
+    float f1, f2, q1;
+    float q2 = 0;
+ 
+    // we need to calculate the limits for SXY specially
+    if (nParam == PM_PAR_SXY) {
+	f1 = 1.0 / PS_SQR(params[PM_PAR_SYY]) + 1.0 / PS_SQR(params[PM_PAR_SXX]);
+	f2 = 1.0 / PS_SQR(params[PM_PAR_SYY]) - 1.0 / PS_SQR(params[PM_PAR_SXX]);
+	q1 = PS_SQR(f1)*AR_RATIO - PS_SQR(f2);
+	assert (q1 > 0);
+	q2  = 0.5*sqrt (q1);
+    }
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT:
+	switch (nParam) {
+	  case PM_PAR_SKY:  beta_lim = 1000;   break;
+	  case PM_PAR_I0:   beta_lim = 10000;    break; // too small?
+	  case PM_PAR_XPOS: beta_lim = 50;     break;
+	  case PM_PAR_YPOS: beta_lim = 50;     break;
+	  case PM_PAR_SXX:  beta_lim = 0.5;    break;
+	  case PM_PAR_SYY:  beta_lim = 0.5;    break;
+	  case PM_PAR_SXY:  beta_lim = 1.0;    break;  // set this to q2?
+	  case 7:           beta_lim = 10.0;     break;
+	  case 8:           beta_lim = M_PI/6.0; break;
+
+	  default:
+	    psAbort("invalid parameter %d for beta test", nParam);
+	}
+	if (fabs(beta[nParam]) > fabs(beta_lim)) {
+	    beta[nParam] = (beta[nParam] > 0) ? fabs(beta_lim) : -fabs(beta_lim);
+	    return false;
+	}
+	return true;
+      case PS_MINIMIZE_PARAM_MIN:
+	switch (nParam) {
+	  case PM_PAR_SKY:  params_min = -1000; break;
+	  case PM_PAR_I0:   params_min =     0; break;
+	  case PM_PAR_XPOS: params_min =  -100; break;
+	  case PM_PAR_YPOS: params_min =  -100; break;
+	  case PM_PAR_SXX:  params_min =   0.5; break;
+	  case PM_PAR_SYY:  params_min =   0.5; break;
+	  case PM_PAR_SXY:  params_min =  -5.0; break; // set this to -q2?
+	  case 7:           params_min =     0;  break;
+	  case 8:           params_min = -1*M_PI; break;
+
+	  default:
+	    psAbort("invalid parameter %d for param min test", nParam);
+	}
+	if (params[nParam] < params_min) {
+	    params[nParam] = params_min;
+	    return false;
+	}
+	return true;
+      case PS_MINIMIZE_PARAM_MAX:
+	switch (nParam) {
+	  case PM_PAR_SKY:  params_max =   1e5; break;
+	  case PM_PAR_I0:   params_max =   1e8; break;
+	  case PM_PAR_XPOS: params_max =   1e4; break;
+	  case PM_PAR_YPOS: params_max =   1e4; break;
+	  case PM_PAR_SXX:  params_max =   100; break;
+	  case PM_PAR_SYY:  params_max =   100; break;
+	  case PM_PAR_SXY:  params_max =   +q2; break;
+	  case 7:           params_max =   150; break;
+	  case 8:           params_max =  M_PI; break;
+	  default:
+	    psAbort("invalid parameter %d for param max test", nParam);
+	}
+	if (params[nParam] > params_max) {
+	    params[nParam] = params_max;
+	    return false;
+	}
+	return true;
+      default:
+	psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+//fixed 
+psF64 PM_MODEL_FLUX(const psVector *params) 
+{ 
+    float f, norm, z; 
+ 
+    psF32 *PAR = params->data.F32; 
+ 
+    psF64 A1   = PS_SQR(PAR[4]); 
+    psF64 A2   = PS_SQR(PAR[5]); 
+    psF64 A3   = PS_SQR(PAR[6]); 
+    psF32 Rx=2./PS_SQR(PAR[4]);  
+    psF32 Ry=2./PS_SQR(PAR[5]);  
+    psF32 Rxy=PAR[6]; 
+ 
+ 
+    psF32 theta = PAR[8];  
+    psF32 sinT=sin(theta); 
+    psF32 cosT=cos(theta); 
+ 
+    psF32 Syrot = ( PS_SQR(sinT)/Rx + PS_SQR(cosT)/Ry - Rxy*sinT*cosT );  //rotated sigma y 
+ 
+    psF64 A4   = Syrot*PAR[7]; 
+ 
+    psF64 Area = 2.0 * M_PI / sqrt(A1*A2 - A3) + A4; 
+    // Area is equivalent to 2 pi sigma^2 + rectangle 
+ 
+    // the area needs to be multiplied by the integral of f(z) 
+    norm = 0.0; 
+    for (z = 0.005; z < 50; z += 0.01) { 
+	f = 1.0 / (1 + z + z*z/2 + z*z*z/6); 
+	norm += f; 
+    } 
+    norm *= 0.01; 
+     
+    psF64 Flux = params->data.F32[1] * Area * norm; 
+ 
+    return(Flux); 
+} 
+ 
+// define this function so it never returns Inf or NaN 
+// also prevent 0 returns, and just send a v small number 
+// return the radius which yields the requested flux 
+ 
+//fixed, but need to change how it is called to accomodate 2 radii 
+psF64 PM_MODEL_RADIUS  (const psVector *params, psF64 flux) 
+{ 
+    if (flux <= 0) return (1.0); 
+    if (params->data.F32[1] <= 0) return (1.0); 
+    if (flux >= params->data.F32[1]) return (1.0); 
+ 
+    psF32 *PAR = params->data.F32; 
+    psF32 sigma  = sqrt(2.0) * hypot (1.0 / params->data.F32[4], 1.0 / params->data.F32[5]); 
+ 
+    psF32 theta = PAR[8];  
+    psF32 sinT=sin(theta); 
+    psF32 cosT=cos(theta); 
+    psF32 Rx=2./PS_SQR(PAR[4]); 
+    psF32 Ry=2./PS_SQR(PAR[5]); 
+    psF32 Rxy=PAR[6]; 
+    psF32 length=PAR[7]; 
+ 
+    psF32 Syrot = ( PS_SQR(sinT)/Rx + PS_SQR(cosT)/Ry - Rxy*sinT*cosT );  //rotated sigma y 
+ 
+    psF64 radius = 0; 
+    if (flux > 0){ 
+      psF64 radius0 = sigma * sqrt (2.0 * log(params->data.F32[1] / flux)); 
+      psF64 radius1 = Syrot * sqrt (2.0 * log(params->data.F32[1] / flux)); 
+ 
+      if (radius0 > radius1) { 
+	radius=radius0+length/2.0; 
+      } else { 
+	radius=radius1+length/2.0; 
+      }  
+    } else { 
+      radius = 1000; 
+    } 
+ 
+    if (radius < 0.01){ 
+      radius = 0.01; 
+    } 
+ 
+    if (isnan(radius)) { 
+      fprintf (stderr, "error in code\n"); 
+    } 
+    return (radius); 
+} 
+ 
+//fixed I think...no good way of guessing as far as I can tell 
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source)
+{ 
+    pmMoments *Smoments = source->moments; 
+    psF32     *params  = model->params->data.F32; 
+ 
+    psEllipseAxes axes; 
+    psEllipseShape shape; 
+    psEllipseMoments moments; 
+ 
+    moments.x2 = PS_SQR(Smoments->Sx); 
+    moments.y2 = PS_SQR(Smoments->Sy); 
+    moments.xy = Smoments->Sxy; 
+    //sometimes these moment inputs are zero...why? 
+ 
+    // solve the math to go from Moments To Shape 
+    // limit the axis ratio to 20 (a guess)
+    axes = psEllipseMomentsToAxes(moments, 20.0); 
+    shape = psEllipseAxesToShape(axes); 
+ 
+    params[0] = Smoments->Sky; 
+    params[1] = Smoments->Peak - Smoments->Sky; 
+    params[2] = Smoments->x; 
+    params[3] = Smoments->y; 
+    params[7] = 2 * axes.major;  
+    params[8] = axes.theta; 
+ 
+    if (moments.x2 == 0 || moments.y2 == 0){ 
+      params[4] = 2.0; 
+      params[5] = 2.0; 
+      params[6] = 0.0; 
+    } else { 
+      params[4] = 1.0 / shape.sx; 
+      params[5] = 1.0 / shape.sy; 
+      params[6] = shape.sxy; 
+    } 
+ 
+    //    printf("Who is NaN? momx: %4.3f  momy: %4.3f  momxy: %4.3f\n", moments.x2,moments.y2, moments.xy); 
+ 
+    return(true); 
+} 
+ 
+//fixed 
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, pmPSF *psf)
+{
+    psF32 *out = modelPSF->params->data.F32; 
+    psF32 *in  = modelFLT->params->data.F32; 
+     
+    out[0] = in[0]; 
+    out[1] = in[1]; 
+    out[2] = in[2]; 
+    out[3] = in[3]; 
+    out[7] = in[7]; 
+    out[8] = in[8]; 
+ 
+    for (int i = 4; i < 7; i++) { 
+      pmTrend2D *trend = psf->params->data[i-4]; 
+	out[i] = pmTrend2DEval (trend, out[2], out[3]); 
+    } 
+    return(true); 
+} 
+ 
+// construct the PSF model from the FLT model and the psf
+// XXX is this sufficiently general do be a global function, not a pmModelClass function?
+bool PM_MODEL_PARAMS_FROM_PSF (pmModel *model, pmPSF *psf, float Xo, float Yo, float Io)
+{
+    psF32 *PAR = model->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    PAR[PM_PAR_SKY]  = 0.0;
+    PAR[PM_PAR_I0]   = Io;
+    PAR[PM_PAR_XPOS] = Xo;
+    PAR[PM_PAR_YPOS] = Yo;
+    
+    // supply the model-fitted parameters, or copy from the input
+    for (int i = 0; i < psf->params->n; i++) {
+	if (i == PM_PAR_SKY) continue;
+	pmTrend2D *trend = psf->params->data[i];
+	PAR[i] = pmTrend2DEval(trend, Xo, Yo);
+    }
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    // XXX user-defined value for limit?
+    if (!pmPSF_FitToModel (PAR, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)", Xo, Yo);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MIN, i, PAR, NULL);
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MAX, i, PAR, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)", Xo, Yo);
+	    model->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+    return(true);
+}
+
+//done I think 
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+    psF32 dP; 
+    bool  status; 
+ 
+    psF32 *PAR  = model->params->data.F32; 
+    psF32 *dPAR = model->dparams->data.F32; 
+ 
+    dP = 0; 
+    dP += PS_SQR(dPAR[4] / PAR[4]); 
+    dP += PS_SQR(dPAR[5] / PAR[5]); 
+    dP = sqrt (dP); 
+ 
+    status = true; 
+    status &= (dP < 0.5); 
+    status &= (PAR[1] > 0); 
+    status &= ((dPAR[1]/PAR[1]) < 0.5); 
+    //    status &= ((dPAR[7]/PAR[7]) < 0.5); 
+    //    status &= ((dPAR[8]/PAR[8]) < 0.5); 
+ 
+    if (status) return true; 
+    return false; 
+} 
+
+# undef PM_MODEL_FUNC
+# undef PM_MODEL_FLUX
+# undef PM_MODEL_GUESS
+# undef PM_MODEL_LIMITS
+# undef PM_MODEL_RADIUS
+# undef PM_MODEL_FROM_PSF
+# undef PM_MODEL_PARAMS_FROM_PSF
+# undef PM_MODEL_FIT_STATUS
Index: /branches/eam_branch_20071023/psphot/src/models/pmModel_TEST1.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/models/pmModel_TEST1.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/models/pmModel_TEST1.c	(revision 15358)
@@ -0,0 +1,311 @@
+/******************************************************************************
+ * this file defines the TEST1 source shape model.  Note that these model functions are loaded
+ * by pmModelGroup.c using 'include', and thus need no 'include' statements of their own.  The
+ * models use a psVector to represent the set of parameters, with the sequence used to specify
+ * the meaning of the parameter.  The meaning of the parameters may thus vary depending on the
+ * specifics of the model.  All models which are used a PSF representations share a few
+ * parameters, for which # define names are listed in pmModel.h:
+
+ * PM_PAR_SKY 0   - local sky : note that this is unused and may be dropped in the future
+ * PM_PAR_I0 1    - central intensity
+ * PM_PAR_XPOS 2  - X center of object
+ * PM_PAR_YPOS 3  - Y center of object
+ * PM_PAR_SXX 4   - X^2 term of elliptical contour (sqrt(2) * SigmaX)
+ * PM_PAR_SYY 5   - Y^2 term of elliptical contour (sqrt(2) * SigmaY)
+ * PM_PAR_SXY 6   - X*Y term of elliptical contour
+ *****************************************************************************/
+
+# define PM_MODEL_FUNC       	  pmModelFunc_TEST1
+# define PM_MODEL_FLUX       	  pmModelFlux_TEST1
+# define PM_MODEL_GUESS      	  pmModelGuess_TEST1
+# define PM_MODEL_LIMITS     	  pmModelLimits_TEST1
+# define PM_MODEL_RADIUS     	  pmModelRadius_TEST1
+# define PM_MODEL_FROM_PSF   	  pmModelFromPSF_TEST1
+# define PM_MODEL_PARAMS_FROM_PSF pmModelParamsFromPSF_TEST1
+# define PM_MODEL_FIT_STATUS      pmModelFitStatus_TEST1
+
+// the model is a function of the pixel coordinate (pixcoord[0,1] = x,y)
+psF32 PM_MODEL_FUNC(psVector *deriv,
+		    const psVector *params,
+		    const psVector *pixcoord)
+{
+    psF32 *PAR = params->data.F32;
+
+    // XXX this is fitting sigma_x/sqrt(2), sigma_y/sqrt(2)
+    psF32 X  = pixcoord->data.F32[0] - PAR[PM_PAR_XPOS];
+    psF32 Y  = pixcoord->data.F32[1] - PAR[PM_PAR_YPOS];
+    psF32 px = X / PAR[PM_PAR_SXX];
+    psF32 py = Y / PAR[PM_PAR_SYY];
+    psF32 z  = PS_SQR(px) + PS_SQR(py) + PAR[PM_PAR_SXY]*X*Y;
+    psF32 t  = 1 + z + z*z/2.0;
+    psF32 r  = 1.0 / (t + z*z*z/6.0); /* exp (-Z) */
+    psF32 f  = PAR[PM_PAR_I0]*r + PAR[PM_PAR_SKY];
+
+    if (deriv != NULL) {
+        psF32 *dPAR = deriv->data.F32;
+        psF32 q = PAR[PM_PAR_I0]*r*r*t;
+        dPAR[PM_PAR_SKY]  = +1.0;
+        dPAR[PM_PAR_I0]   = +r;
+        dPAR[PM_PAR_XPOS] = q*(2.0*px/PAR[PM_PAR_SXX] + Y*PAR[PM_PAR_SXY]);
+        dPAR[PM_PAR_YPOS] = q*(2.0*py/PAR[PM_PAR_SYY] + X*PAR[PM_PAR_SXY]);
+        dPAR[PM_PAR_SXX]  = +2.0*q*px*px/PAR[PM_PAR_SXX];
+        dPAR[PM_PAR_SYY]  = +2.0*q*py*py/PAR[PM_PAR_SYY];
+        dPAR[PM_PAR_SXY]  = -q*X*Y;
+    }
+    return(f);
+}
+
+// define the parameter limits
+bool PM_MODEL_LIMITS (psMinConstraintMode mode, int nParam, float *params, float *beta) 
+{
+    float beta_lim = 0;
+    float params_min = 0;
+    float params_max = 0;
+
+    switch (mode) {
+      case PS_MINIMIZE_BETA_LIMIT:
+	switch (nParam) {
+	  case PM_PAR_SKY:  beta_lim = 1000;  break;
+	  case PM_PAR_I0:   beta_lim = 3e6;   break;
+	  case PM_PAR_XPOS: beta_lim = 5;     break;
+	  case PM_PAR_YPOS: beta_lim = 5;     break;
+	  case PM_PAR_SXX:  beta_lim = 0.5;   break;
+	  case PM_PAR_SYY:  beta_lim = 0.5;   break;
+	  case PM_PAR_SXY:  beta_lim = 0.5;   break;
+	  default:
+	    psAbort("invalid parameter %d for beta test", nParam);
+	}
+	if (fabs(beta[nParam]) > fabs(beta_lim)) {
+	    beta[nParam] = (beta[nParam] > 0) ? fabs(beta_lim) : -fabs(beta_lim);
+	    return false;
+	}
+	return true;
+      case PS_MINIMIZE_PARAM_MIN:
+	switch (nParam) {
+	  case PM_PAR_SKY:  params_min = -1000; break;
+	  case PM_PAR_I0:   params_min =     0; break;
+	  case PM_PAR_XPOS: params_min =  -100; break;
+	  case PM_PAR_YPOS: params_min =  -100; break;
+	  case PM_PAR_SXX:  params_min =   0.5; break;
+	  case PM_PAR_SYY:  params_min =   0.5; break;
+	  case PM_PAR_SXY:  params_min =  -5.0; break;
+	  default:
+	    psAbort("invalid parameter %d for param min test", nParam);
+	}
+	if (params[nParam] < params_min) {
+	    params[nParam] = params_min;
+	    return false;
+	}
+	return true;
+      case PS_MINIMIZE_PARAM_MAX:
+	switch (nParam) {
+	  case PM_PAR_SKY:  params_max =   1e5; break;
+	  case PM_PAR_I0:   params_max =   1e8; break;
+	  case PM_PAR_XPOS: params_max =   1e4; break;
+	  case PM_PAR_YPOS: params_max =   1e4; break;
+	  case PM_PAR_SXX:  params_max =   100; break;
+	  case PM_PAR_SYY:  params_max =   100; break;
+	  case PM_PAR_SXY:  params_max =  +5.0; break;
+	  default:
+	    psAbort("invalid parameter %d for param max test", nParam);
+	}
+	if (params[nParam] > params_max) {
+	    params[nParam] = params_max;
+	    return false;
+	}
+	return true;
+      default:
+	psAbort("invalid choice for limits");
+    }
+    psAbort("should not reach here");
+    return false;
+}
+
+// make an initial guess for parameters
+bool PM_MODEL_GUESS (pmModel *model, pmSource *source)
+{
+    pmMoments *moments = source->moments;
+    psF32     *PAR  = model->params->data.F32;
+
+    PAR[PM_PAR_SKY] = moments->Sky;
+    PAR[PM_PAR_I0] = moments->Peak - moments->Sky;
+    PAR[PM_PAR_XPOS] = moments->x;
+    PAR[PM_PAR_YPOS] = moments->y;
+    PAR[PM_PAR_SXX] = PS_MAX(0.5, moments->Sx);
+    PAR[PM_PAR_SYY] = PS_MAX(0.5, moments->Sy);
+    PAR[PM_PAR_SXY] = 0.0;  // XXX we can get this right if we do the integral
+
+    return(true);
+}
+
+psF64 PM_MODEL_FLUX(const psVector *params)
+{
+    float norm, z;
+    psEllipseShape shape;
+
+    psF32 *PAR = params->data.F32;
+
+    shape.sx  = PAR[PM_PAR_SXX] / sqrt(2.0);
+    shape.sy  = PAR[PM_PAR_SYY] / sqrt(2.0);
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    // Area is equivalent to 2 pi sigma^2
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 Area = 2.0 * M_PI * axes.major * axes.minor;
+
+    // the area needs to be multiplied by the integral of f(z)
+    norm = 0.0;
+
+# define DZ 0.25
+
+    float f0 = 1.0;
+    float f1, f2;
+    for (z = DZ; z < 50; z += DZ) {
+        f1 = 1.0 / (1 + z + z*z/2.0 + z*z*z/6.0);
+        z += DZ;
+        f2 = 1.0 / (1 + z + z*z/2.0 + z*z*z/6.0);
+        norm += f0 + 4*f1 + f2;
+        f0 = f2;
+    }
+    norm *= DZ / 3.0;
+
+    psF64 Flux = PAR[PM_PAR_I0] * Area * norm;
+
+    return(Flux);
+}
+
+// define this function so it never returns Inf or NaN
+// return the radius which yields the requested flux
+psF64 PM_MODEL_RADIUS (const psVector *params, psF64 flux)
+{
+    psEllipseShape shape;
+
+    if (flux <= 0)
+        return (1.0);
+    if (params->data.F32[PM_PAR_I0] <= 0)
+        return (1.0);
+    if (flux >= params->data.F32[PM_PAR_I0])
+        return (1.0);
+
+    psF32 *PAR = params->data.F32;
+
+    shape.sx  = PAR[PM_PAR_SXX] / sqrt(2.0);
+    shape.sy  = PAR[PM_PAR_SYY] / sqrt(2.0);
+    shape.sxy = PAR[PM_PAR_SXY];
+
+    // this estimates the radius assuming f(z) is roughly exp(-z)
+    psEllipseAxes axes = psEllipseShapeToAxes (shape, 20.0);
+    psF64 radius = axes.major * sqrt (2.0 * log(params->data.F32[PM_PAR_I0] / flux));
+
+    if (isnan(radius)) psAbort("error in code: never return invalid radius");
+    if (radius < 0) psAbort("error in code: never return invalid radius");
+
+    return (radius);
+}
+
+bool PM_MODEL_FROM_PSF (pmModel *modelPSF, pmModel *modelFLT, pmPSF *psf)
+{
+    psF32 *out = modelPSF->params->data.F32;
+    psF32 *in  = modelFLT->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    for (int i = 0; i < psf->params->n; i++) {
+	if (psf->params->data[i] == NULL) {
+	    out[i] = in[i];
+	} else {	    
+	    pmTrend2D *trend = psf->params->data[i];
+	    out[i] = pmTrend2DEval(trend, in[PM_PAR_XPOS], in[PM_PAR_YPOS]);
+	}
+    }
+
+    // the 2D model for SXY actually fits SXY / (SXX^-2 + SYY^-2); correct here
+    out[PM_PAR_SXY] = pmPSF_SXYtoModel (out);
+
+    return(true);
+}
+
+// construct the PSF model from the FLT model and the psf
+// XXX is this sufficiently general do be a global function, not a pmModelClass function?
+bool PM_MODEL_PARAMS_FROM_PSF (pmModel *model, pmPSF *psf, float Xo, float Yo, float Io)
+{
+    psF32 *PAR = model->params->data.F32;
+
+    // we require these two parameters to exist
+    assert (psf->params->n > PM_PAR_YPOS);
+    assert (psf->params->n > PM_PAR_XPOS);
+
+    PAR[PM_PAR_SKY]  = 0.0;
+    PAR[PM_PAR_I0]   = Io;
+    PAR[PM_PAR_XPOS] = Xo;
+    PAR[PM_PAR_YPOS] = Yo;
+    
+    // supply the model-fitted parameters, or copy from the input
+    for (int i = 0; i < psf->params->n; i++) {
+	if (i == PM_PAR_SKY) continue;
+	pmTrend2D *trend = psf->params->data[i];
+	PAR[i] = pmTrend2DEval(trend, Xo, Yo);
+    }
+
+    // the 2D PSF model fits polarization terms (E0,E1,E2)
+    // convert to shape terms (SXX,SYY,SXY)
+    // XXX user-defined value for limit?
+    if (!pmPSF_FitToModel (PAR, 0.1)) {
+	psError(PM_ERR_PSF, false, "Failed to fit object at (r,c) = (%.1f,%.1f)", Xo, Yo);
+	return false;
+    }
+
+    // apply the model limits here: this truncates excessive extrapolation
+    // XXX do we need to do this still?  should we put in asserts to test?
+    for (int i = 0; i < psf->params->n; i++) {
+        // apply the limits to all components or just the psf-model parameters?
+        if (psf->params->data[i] == NULL)
+            continue;
+
+	bool status = true;
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MIN, i, PAR, NULL);
+        status &= PM_MODEL_LIMITS (PS_MINIMIZE_PARAM_MAX, i, PAR, NULL);
+	if (!status) {
+	    psTrace ("psModules.objects", 5, "Hitting parameter limits at (r,c) = (%.1f, %.1f)", Xo, Yo);
+	    model->flags |= PM_MODEL_STATUS_LIMITS;
+	}
+    }
+    return(true);
+}
+
+// XXX double-check these definitions below
+// this test is invalid if the parameters are derived
+// from the PSF model
+bool PM_MODEL_FIT_STATUS (pmModel *model)
+{
+    psF32 dP;
+    bool  status;
+
+    psF32 *PAR  = model->params->data.F32;
+    psF32 *dPAR = model->dparams->data.F32;
+
+    dP = 0;
+    dP += PS_SQR(dPAR[PM_PAR_SXX] / PAR[PM_PAR_SXX]);
+    dP += PS_SQR(dPAR[PM_PAR_SYY] / PAR[PM_PAR_SYY]);
+    dP = sqrt (dP);
+
+    status = true;
+    status &= (dP < 0.5);
+    status &= (PAR[PM_PAR_I0] > 0);
+    status &= ((dPAR[PM_PAR_I0]/PAR[PM_PAR_I0]) < 0.5);
+
+    if (status)
+        return true;
+    return false;
+}
+
+# undef PM_MODEL_FUNC
+# undef PM_MODEL_FLUX
+# undef PM_MODEL_GUESS
+# undef PM_MODEL_LIMITS
+# undef PM_MODEL_RADIUS
+# undef PM_MODEL_FROM_PSF
+# undef PM_MODEL_PARAMS_FROM_PSF
+# undef PM_MODEL_FIT_STATUS
Index: /branches/eam_branch_20071023/psphot/src/pmFootprint.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/pmFootprint.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/pmFootprint.c	(revision 15358)
@@ -0,0 +1,1242 @@
+/*****************************************************************************/
+/*
+ * Handle pmFootprints
+ */
+#include <assert.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "psphot.h"
+#include "pmFootprint.h"
+
+static void spanFree(pmSpan *tmp) {
+   ;
+}
+
+/*
+ * pmSpanAlloc()
+ */
+pmSpan *pmSpanAlloc(int y, int x0, int x1)
+{
+    pmSpan *span = (pmSpan *)psAlloc(sizeof(pmSpan));
+    psMemSetDeallocator(span, (psFreeFunc) spanFree);
+
+    span->y = y;
+    span->x0 = x0;
+    span->x1 = x1;
+
+    return(span);
+}
+
+bool pmSpanTest(const psPtr ptr)
+{
+    return (psMemGetDeallocator(ptr) == (psFreeFunc)spanFree);
+}
+
+//
+// Sort pmSpans by y, then x0, then x1
+//
+int pmSpanSortByYX(const void **a, const void **b) {
+    const pmSpan *sa = *(const pmSpan **)a;
+    const pmSpan *sb = *(const pmSpan **)b;
+
+    if (sa->y < sb->y) {
+	return -1;
+    } else if (sa->y == sb->y) {
+	if (sa->x0 < sb->x0) {
+	    return -1;
+	} else if (sa->x0 == sb->x0) {
+	    if (sa->x1 < sb->x1) {
+		return -1;
+	    } else if (sa->x1 == sb->x1) {
+		return 0;
+	    } else {
+		return 1;
+	    }
+	} else {
+	    return 1;
+	}
+    } else {
+	return 1;
+    }
+}
+
+/************************************************************************************************************/
+
+static void footprintFree(pmFootprint *tmp)
+{
+   if (!tmp) {
+        return;
+   }
+
+   psTrace("psModules.objects", 5, "---- begin ----\n");
+
+   psFree(tmp->spans);
+   psFree(tmp->peaks);
+
+   psTrace("psModules.objects", 5, "---- end ----\n");
+}
+
+/*
+ * pmFootprintAlloc(): Allocate the pmFootprint structure to NULL.
+ */
+pmFootprint *pmFootprintAlloc(int nspan, // number of spans expected in footprint
+			      const psImage *image) // region footprint lives in
+{
+    psTrace("psModules.objects", 5, "---- begin ----\n");
+
+    static int id = 1;
+    pmFootprint *footprint = (pmFootprint *)psAlloc(sizeof(pmFootprint));
+    *(int *)&footprint->id = id++;
+    psMemSetDeallocator(footprint, (psFreeFunc) footprintFree);
+
+    footprint->normalized = false;
+
+    assert(nspan >= 0);
+    footprint->npix = 0;
+    footprint->spans = psArrayAllocEmpty(nspan);
+    footprint->peaks = psArrayAlloc(0);
+
+    footprint->bbox.x0 = footprint->bbox.y0 = 0;
+    footprint->bbox.x1 = footprint->bbox.y1 = -1;
+
+    if (image == NULL) {
+	footprint->region.x0 = footprint->region.y0 = 0;
+	footprint->region.x1 = footprint->region.y1 = -1;
+    } else {
+	footprint->region.x0 = image->col0;
+	footprint->region.x1 = image->col0 + image->numCols - 1;
+	footprint->region.y0 = image->row0;
+	footprint->region.y1 = image->row0 + image->numRows - 1;
+    }
+
+    psTrace("psModules.objects", 5, "---- end ----\n");
+    return(footprint);
+}
+
+bool pmFootprintTest(const psPtr ptr) {
+    return (psMemGetDeallocator(ptr) == (psFreeFunc)footprintFree);
+}
+
+pmFootprint *pmFootprintNormalize(pmFootprint *fp) {
+    if (fp != NULL && !fp->normalized) {
+	fp->peaks = psArraySort(fp->peaks, pmPeakSortBySN);
+	fp->normalized = true;
+    }
+
+    return fp;
+}
+
+//
+// Add a span to a footprint, returning the new span
+//
+static pmSpan *pmFootprintAddSpan(pmFootprint *fp,	// the footprint to add to
+				  const int y, // row to add
+				  int x0,      // range of
+				  int x1) {    //          columns
+
+    if (x1 < x0) {
+	int tmp = x0;
+	x0 = x1;
+	x1 = tmp;
+    }
+
+    pmSpan *sp = pmSpanAlloc(y, x0, x1);
+    psArrayAdd(fp->spans, 1, sp);
+    psFree(sp);
+    
+    fp->npix += x1 - x0 + 1;
+
+    if (fp->spans->n == 1) {
+	fp->bbox.x0 = x0;
+	fp->bbox.x1 = x1;
+	fp->bbox.y0 = y;
+	fp->bbox.y1 = y;
+    } else {
+	if (x0 < fp->bbox.x0) fp->bbox.x0 = x0;
+	if (x1 > fp->bbox.x1) fp->bbox.x1 = x1;
+	if (y < fp->bbox.y0) fp->bbox.y0 = y;
+	if (y > fp->bbox.y1) fp->bbox.y1 = y;
+    }
+
+    return sp;
+}
+
+void pmFootprintSetBBox(pmFootprint *fp) {
+    assert (fp != NULL);
+    if (fp->spans->n == 0) {
+	return;
+    }
+    pmSpan *sp = fp->spans->data[0];
+    int x0 = sp->x0;
+    int x1 = sp->x1;
+    int y0 = sp->y;
+    int y1 = sp->y;
+
+    for (int i = 1; i < fp->spans->n; i++) {
+	sp = fp->spans->data[i];
+	
+	if (sp->x0 < x0) x0 = sp->x0;
+	if (sp->x1 > x1) x1 = sp->x1;
+	if (sp->y < y0) y0 = sp->y;
+	if (sp->y > y1) y1 = sp->y;
+    }
+
+    fp->bbox.x0 = x0;
+    fp->bbox.x1 = x1;
+    fp->bbox.y0 = y0;
+    fp->bbox.y1 = y1;
+}
+
+int pmFootprintSetNpix(pmFootprint *fp) {
+   assert (fp != NULL);
+   int npix = 0;
+   for (int i = 0; i < fp->spans->n; i++) {
+       pmSpan *span = fp->spans->data[i];
+       npix += span->x1 - span->x0 + 1;
+   }
+   fp->npix = npix;
+
+   return npix;
+}
+
+/************************************************************************************************************/
+
+typedef struct {			/* run-length code for part of object*/
+   int id;				/* ID for object */
+   int y;				/* Row wherein WSPAN dwells */
+   int x0, x1;				/* inclusive range of columns */
+} WSPAN;
+
+/*
+ * comparison function for qsort; sort by ID then row
+ */
+static int
+compar(const void *va, const void *vb)
+{
+   const WSPAN *a = va;
+   const WSPAN *b = vb;
+
+   if(a->id < b->id) {
+      return(-1);
+   } else if(a->id > b->id) {
+      return(1);
+   } else {
+      return(a->y - b->y);
+   }
+}
+
+/*
+ * Follow a chain of aliases, returning the final resolved value.
+ */
+static int
+resolve_alias(const int *aliases,	/* list of aliases */
+	      int id)			/* alias to look up */
+{
+   int resolved = id;			/* resolved alias */
+
+   while(id != aliases[id]) {
+      resolved = id = aliases[id];
+   }
+
+   return(resolved);
+}
+
+/*
+ * Go through an image, finding sets of connected pixels above threshold
+ * and assembling them into pmFootprints;  the resulting set of objects
+ * is returned as a psArray
+ */
+psArray *
+pmFindFootprints(const psImage *img,	// image to search
+		 const float threshold,	// Threshold
+		 const int npixMin)	// minimum number of pixels in an acceptable pmFootprint
+{
+   int i0;				/* initial value of i */
+   int id;				/* object ID */
+   int in_span;				/* object ID of current WSPAN */
+   int nspan = 0;			/* number of spans */
+   int nobj = 0;			/* number of objects found */
+   int x0 = 0;			        /* unpacked from a WSPAN */
+   int *tmp;				/* used in swapping idc/idp */
+
+   assert(img != NULL);
+
+   bool F32 = false;			// is this an F32 image?
+   if (img->type.type == PS_TYPE_F32) {
+       F32 = true;
+   } else if (img->type.type == PS_TYPE_S32) {
+       F32 = false;
+   } else {				// N.b. You can't trivially add more cases here; F32 is just a bool
+       psError(PS_ERR_UNKNOWN, true, "Unsupported psImage type: %d", img->type.type);
+       return NULL;
+   }
+   psF32 *imgRowF32 = NULL;		// row pointer if F32
+   psS32 *imgRowS32 = NULL;		//  "   "   "  "  !F32
+   
+   const int row0 = img->row0;
+   const int col0 = img->col0;
+   const int numRows = img->numRows;
+   const int numCols = img->numCols;
+/*
+ * Storage for arrays that identify objects by ID. We want to be able to
+ * refer to idp[-1] and idp[numCols], hence the (numCols + 2)
+ */
+   int *id_s = psAlloc(2*(numCols + 2)*sizeof(int));
+   memset(id_s, '\0', 2*(numCols + 2)*sizeof(int)); assert(id_s[0] == 0);
+   int *idc = id_s + 1;			// object IDs in current/
+   int *idp = idc + (numCols + 2);	//                       previous row
+
+   int size_aliases = 1 + numRows/20;	// size of aliases[] array
+   int *aliases = psAlloc(size_aliases*sizeof(int)); // aliases for object IDs
+
+   int size_spans = 1 + numRows/20;	// size of spans[] array
+   WSPAN *spans = psAlloc(size_spans*sizeof(WSPAN)); // row:x0,x1 for objects
+/*
+ * Go through image identifying objects
+ */
+   for (int i = 0; i < numRows; i++) {
+      int j;
+      tmp = idc; idc = idp; idp = tmp;	/* swap ID pointers */
+      memset(idc, '\0', numCols*sizeof(int));
+      
+      imgRowF32 = img->data.F32[i];	// only one of
+      imgRowS32 = img->data.S32[i];	//      these is valid!
+
+      in_span = 0;			/* not in a span */
+      for (j = 0; j < numCols; j++) {
+	 double pixVal = F32 ? imgRowF32[j] : imgRowS32[j];
+	 if (pixVal < threshold) {
+	    if (in_span) {
+	       if(nspan >= size_spans) {
+		  size_spans *= 2;
+		  spans = psRealloc(spans, size_spans*sizeof(WSPAN));
+	       }
+	       spans[nspan].id = in_span;
+	       spans[nspan].y = i;
+	       spans[nspan].x0 = x0;
+	       spans[nspan].x1 = j - 1;
+	       
+	       nspan++;
+
+	       in_span = 0;
+	    }
+	 } else {			/* a pixel to fix */
+	    if(idc[j - 1] != 0) {
+	       id = idc[j - 1];
+	    } else if(idp[j - 1] != 0) {
+	       id = idp[j - 1];
+	    } else if(idp[j] != 0) {
+	       id = idp[j];
+	    } else if(idp[j + 1] != 0) {
+	       id = idp[j + 1];
+	    } else {
+	       id = ++nobj;
+
+	       if(id >= size_aliases) {
+		  size_aliases *= 2;
+		  aliases = psRealloc(aliases, size_aliases*sizeof(int));
+	       }
+	       aliases[id] = id;
+	    }
+
+	    idc[j] = id;
+	    if(!in_span) {
+	       x0 = j; in_span = id;
+	    }
+/*
+ * Do we need to merge ID numbers? If so, make suitable entries in aliases[]
+ */
+	    if(idp[j + 1] != 0 && idp[j + 1] != id) {
+	       aliases[resolve_alias(aliases, idp[j + 1])] =
+						    resolve_alias(aliases, id);
+	       
+	       idc[j] = id = idp[j + 1];
+	    }
+	 }
+      }
+
+      if(in_span) {
+	 if(nspan >= size_spans) {
+	    size_spans *= 2;
+	    spans = psRealloc(spans, size_spans*sizeof(WSPAN));
+	 }
+
+	 assert(nspan < size_spans);	/* we checked for space above */
+	 spans[nspan].id = in_span;
+	 spans[nspan].y = i;
+	 spans[nspan].x0 = x0;
+	 spans[nspan].x1 = j - 1;
+	 
+	 nspan++;
+      }
+   }
+
+   psFree(id_s);
+/*
+ * Resolve aliases; first alias chains, then the IDs in the spans
+ */
+   for (int i = 0; i < nspan; i++) {
+      spans[i].id = resolve_alias(aliases, spans[i].id);
+   }
+
+   psFree(aliases);
+/*
+ * Sort spans by ID, so we can sweep through them once
+ */
+   if(nspan > 0) {
+      qsort(spans, nspan, sizeof(WSPAN), compar);
+   }
+/*
+ * Build pmFootprints from the spans
+ */
+   psArray *footprints = psArrayAlloc(nobj);
+   int n = 0;			// number of pmFootprints
+
+   if(nspan > 0) {
+      id = spans[0].id;
+      i0 = 0;
+      for (int i = 0; i <= nspan; i++) {	/* nspan + 1 to catch last object */
+	 if(i == nspan || spans[i].id != id) {
+	    pmFootprint *fp = pmFootprintAlloc(i - i0, img);
+	    
+	    for(; i0 < i; i0++) {
+		pmFootprintAddSpan(fp, spans[i0].y + row0,
+				   spans[i0].x0 + col0, spans[i0].x1 + col0);
+	    }
+
+	    if (fp->npix < npixMin) {
+	       psFree(fp);
+	    } else {
+	       footprints->data[n++] = fp;
+	    }
+	 }
+	 
+	 id = spans[i].id;
+      }
+   }
+
+   footprints = psArrayRealloc(footprints, n);
+/*
+ * clean up
+ */
+   psFree(spans);
+
+   return footprints;
+}
+
+/************************************************************************************************************/
+/*
+ * A data structure to hold the starting point for a search for pixels above threshold,
+ * used by pmFindFootprintAtPoint
+ *
+ * We don't want to find this span again --- it's already part of the footprint ---
+ * so we set appropriate mask bits
+ */
+//
+// An enum for what we should do with a Startspan
+//
+typedef enum {PM_SSPAN_DOWN = 0,	// scan down from this span
+	      PM_SSPAN_UP,		// scan up from this span
+	      PM_SSPAN_RESTART,		// restart scanning from this span
+	      PM_SSPAN_DONE		// this span is processed
+} PM_SSPAN_DIR;				// How to continue searching
+//
+// An enum for mask's pixel values.  We're looking for pixels that are above threshold, and
+// we keep extra book-keeping information in the PM_SSPAN_STOP plane.  It's simpler to be
+// able to check for 
+//
+enum {
+    PM_SSPAN_INITIAL = 0x0,		// initial state of pixels.
+    PM_SSPAN_DETECTED = 0x1,		// we've seen this pixel
+    PM_SSPAN_STOP = 0x2			// you may stop searching when you see this pixel
+};
+//
+// The struct that remembers how to [re-]start scanning the image for pixels
+//
+typedef struct {
+    const pmSpan *span;			// save the pixel range
+    PM_SSPAN_DIR direction;		// How to continue searching
+    bool stop;				// should we stop searching?
+} Spartspan;
+
+static void startspanFree(Spartspan *sspan) {
+    psFree((void *)sspan->span);
+}
+
+static Spartspan *
+SpartspanAlloc(const pmSpan *span,	// The span in question
+	       psImage *mask,		// Pixels that we've already detected
+	       const PM_SSPAN_DIR dir	// Should we continue searching towards the top of the image?
+    ) {
+    Spartspan *sspan = psAlloc(sizeof(Spartspan));
+    psMemSetDeallocator(sspan, (psFreeFunc)startspanFree);
+
+    sspan->span = psMemIncrRefCounter((void *)span);
+    sspan->direction = dir;
+    sspan->stop = false;
+    
+    if (mask != NULL) {			// remember that we've detected these pixels
+	psMaskType *mpix = &mask->data.PS_TYPE_MASK_DATA[span->y - mask->row0][span->x0 - mask->col0];
+
+	for (int i = 0; i <= span->x1 - span->x0; i++) {
+	    mpix[i] |= PM_SSPAN_DETECTED;
+	    if (mpix[i] & PM_SSPAN_STOP) {
+		sspan->stop = true;
+	    }
+	}
+    }
+    
+    return sspan;
+}
+
+//
+// Add a new Spartspan to an array of Spartspans.  Iff we see a stop bit, return true
+//
+static bool add_startspan(psArray *startspans, // the saved Spartspans
+			  const pmSpan *sp, // the span in question
+			  psImage *mask, // mask of detected/stop pixels
+			  const PM_SSPAN_DIR dir) { // the desired direction to search
+    if (dir == PM_SSPAN_RESTART) {
+	if (add_startspan(startspans, sp, mask,  PM_SSPAN_UP) ||
+	    add_startspan(startspans, sp, NULL, PM_SSPAN_DOWN)) {
+	    return true;
+	}
+    } else {
+	Spartspan *sspan = SpartspanAlloc(sp, mask, dir);
+	if (sspan->stop) {		// we detected a stop bit
+	    psFree(sspan);		// don't allocate new span
+
+	    return true;
+	} else {
+	    psArrayAdd(startspans, 1, sspan);
+	    psFree(sspan);		// as it's now owned by startspans
+	}
+    }
+
+    return false;
+}
+
+/************************************************************************************************************/
+/*
+ * Search the image for pixels above threshold, starting at a single Spartspan.
+ * We search the array looking for one to process; it'd be better to move the
+ * ones that we're done with to the end, but it probably isn't worth it for
+ * the anticipated uses of this routine.
+ *
+ * This is the guts of pmFindFootprintAtPoint
+ */
+static bool do_startspan(pmFootprint *fp, // the footprint that we're building
+			 const psImage *img, // the psImage we're working on
+			 psImage *mask, // the associated masks
+			 const float threshold,	// Threshold
+			 psArray *startspans) {	// specify which span to process next
+    bool F32 = false;			// is this an F32 image?
+    if (img->type.type == PS_TYPE_F32) {
+	F32 = true;
+    } else if (img->type.type == PS_TYPE_S32) {
+	F32 = false;
+    } else {				// N.b. You can't trivially add more cases here; F32 is just a bool
+	psError(PS_ERR_UNKNOWN, true, "Unsupported psImage type: %d", img->type.type);
+	return NULL;
+    }
+
+    psF32 *imgRowF32 = NULL;		// row pointer if F32
+    psS32 *imgRowS32 = NULL;		//  "   "   "  "  !F32
+    psMaskType *maskRow = NULL;		//  masks's row pointer
+    
+    const int row0 = img->row0;
+    const int col0 = img->col0;
+    const int numRows = img->numRows;
+    const int numCols = img->numCols;
+    
+    /********************************************************************************************************/
+    
+    Spartspan *sspan = NULL;
+    for (int i = 0; i < startspans->n; i++) {
+	sspan = startspans->data[i];
+	if (sspan->direction != PM_SSPAN_DONE) {
+	    break;
+	}
+	if (sspan->stop) {
+	    break;
+	}
+    }
+    if (sspan == NULL || sspan->direction == PM_SSPAN_DONE) { // no more Spartspans to process
+	return false;
+    }
+    if (sspan->stop) {			// they don't want any more spans processed
+	return false;
+    }
+    /*
+     * Work
+     */
+    const PM_SSPAN_DIR dir = sspan->direction;
+    /*
+     * Set initial span to the startspan
+     */
+    int x0 = sspan->span->x0 - col0, x1 = sspan->span->x1 - col0;
+    /*
+     * Go through image identifying objects
+     */
+    int nx0, nx1 = -1;			// new values of x0, x1
+    const int di = (dir == PM_SSPAN_UP) ? 1 : -1; // how much i changes to get to the next row
+    bool stop = false;			// should I stop searching for spans?
+
+    for (int i = sspan->span->y -row0 + di; i < numRows && i >= 0; i += di) {
+	imgRowF32 = img->data.F32[i];	// only one of
+	imgRowS32 = img->data.S32[i];	//      these is valid!
+	maskRow = mask->data.PS_TYPE_MASK_DATA[i];
+	//
+	// Search left from the pixel diagonally to the left of (i - di, x0). If there's
+	// a connected span there it may need to grow up and/or down, so push it onto
+	// the stack for later consideration
+	//
+	nx0 = -1;
+	for (int j = x0 - 1; j >= -1; j--) {
+	    double pixVal = (j < 0) ? threshold - 100 : (F32 ? imgRowF32[j] : imgRowS32[j]);
+	    if ((maskRow[j] & PM_SSPAN_DETECTED) || pixVal < threshold) {
+		if (j < x0 - 1) {	// we found some pixels above threshold
+		    nx0 = j + 1;
+		}
+		break;
+	    }
+	}
+
+	if (nx0 < 0) {			// no span to the left
+	    nx1 = x0 - 1;		// we're going to resume searching at nx1 + 1
+	} else {
+	    //
+	    // Search right in leftmost span
+	    //
+	    //nx1 = 0;			// make gcc happy
+	    for (int j = nx0 + 1; j <= numCols; j++) {
+		double pixVal = (j >= numCols) ? threshold - 100 : (F32 ? imgRowF32[j] : imgRowS32[j]);
+		if ((maskRow[j] & PM_SSPAN_DETECTED) || pixVal < threshold) {
+		    nx1 = j - 1;
+		    break;
+		}
+	    }
+	    
+	    const pmSpan *sp = pmFootprintAddSpan(fp, i + row0, nx0 + col0, nx1 + col0);
+	    
+	    if (add_startspan(startspans, sp, mask, PM_SSPAN_RESTART)) {
+		stop = true;
+		break;
+	    }
+	}
+	//
+	// Now look for spans connected to the old span.  The first of these we'll
+	// simply process, but others will have to be deferred for later consideration.
+	//
+	// In fact, if the span overhangs to the right we'll have to defer the overhang
+	// until later too, as it too can grow in both directions
+	//
+	// Note that column numCols exists virtually, and always ends the last span; this
+	// is why we claim below that sx1 is always set
+	//
+	bool first = false;		// is this the first new span detected?
+	for (int j = nx1 + 1; j <= x1 + 1; j++) {
+	    double pixVal = (j >= numCols) ? threshold - 100 : (F32 ? imgRowF32[j] : imgRowS32[j]);
+	    if (!(maskRow[j] & PM_SSPAN_DETECTED) && pixVal >= threshold) {
+		int sx0 = j++;		// span that we're working on is sx0:sx1
+		int sx1 = -1;		// We know that if we got here, we'll also set sx1
+		for (; j <= numCols; j++) {
+		    double pixVal = (j >= numCols) ? threshold - 100 : (F32 ? imgRowF32[j] : imgRowS32[j]);
+		    if ((maskRow[j] & PM_SSPAN_DETECTED) || pixVal < threshold) { // end of span
+			sx1 = j;
+			break;
+		    }
+		}
+		assert (sx1 >= 0);
+
+		const pmSpan *sp;
+		if (first) {
+		    if (sx1 <= x1) {
+			sp = pmFootprintAddSpan(fp, i + row0, sx0 + col0, sx1 + col0 - 1);
+			if (add_startspan(startspans, sp, mask, PM_SSPAN_DONE)) {
+			    stop = true;
+			    break;
+			}
+		    } else {		// overhangs to right
+			sp = pmFootprintAddSpan(fp, i + row0, sx0 + col0, x1 + col0);
+			if (add_startspan(startspans, sp, mask, PM_SSPAN_DONE)) {
+			    stop = true;
+			    break;
+			}
+			sp = pmFootprintAddSpan(fp, i + row0, x1 + 1 + col0, sx1 + col0 - 1);
+			if (add_startspan(startspans, sp, mask, PM_SSPAN_RESTART)) {
+			    stop = true;
+			    break;
+			}
+		    }
+		    first = false;
+		} else {
+		    sp = pmFootprintAddSpan(fp, i + row0, sx0 + col0, sx1 + col0 - 1);
+		    if (add_startspan(startspans, sp, mask, PM_SSPAN_RESTART)) {
+			stop = true;
+			break;
+		    }
+		}
+	    }
+	}
+
+	if (stop || first == false) {	// we're done
+	    break;
+	}
+
+	x0 = nx0; x1 = nx1;
+    }
+    /*
+     * Cleanup
+     */
+
+    sspan->direction = PM_SSPAN_DONE;
+    return stop ? false : true;
+}
+
+/*
+ * Go through an image, starting at (row, col) and assembling all the pixels
+ * that are connected to that point (in a chess kings-move sort of way) into
+ * a pmFootprint.
+ *
+ * This is much slower than pmFindFootprints if you want to find lots of
+ * footprints, but if you only want a small region about a given point it
+ * can be much faster
+ *
+ * N.b. The returned pmFootprint is not in "normal form"; that is the pmSpans
+ * are not sorted by increasing y, x0, x1.  If this matters to you, call
+ * pmFootprintNormalize()
+ */
+pmFootprint *
+pmFindFootprintAtPoint(const psImage *img,	// image to search
+		       const float threshold,	// Threshold
+		       const psArray *peaks, // array of peaks; finding one terminates search for footprint
+		       int row, int col) { // starting position (in img's parent's coordinate system)
+   assert(img != NULL);
+
+   bool F32 = false;			// is this an F32 image?
+   if (img->type.type == PS_TYPE_F32) {
+       F32 = true;
+   } else if (img->type.type == PS_TYPE_S32) {
+       F32 = false;
+   } else {				// N.b. You can't trivially add more cases here; F32 is just a bool
+       psError(PS_ERR_UNKNOWN, true, "Unsupported psImage type: %d", img->type.type);
+       return NULL;
+   }
+   psF32 *imgRowF32 = NULL;		// row pointer if F32
+   psS32 *imgRowS32 = NULL;		//  "   "   "  "  !F32
+   
+   const int row0 = img->row0;
+   const int col0 = img->col0;
+   const int numRows = img->numRows;
+   const int numCols = img->numCols;
+/*
+ * Is point in image, and above threshold?
+ */
+   row -= row0; col -= col0;
+   if (row < 0 || row >= numRows ||
+       col < 0 || col >= numCols) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "row/col == (%d, %d) are out of bounds [%d--%d, %d--%d]",
+		row + row0, col + col0, row0, row0 + numRows - 1, col0, col0 + numCols - 1);
+       return NULL;
+   }
+
+   double pixVal = F32 ? img->data.F32[row][col] : img->data.S32[row][col];
+   if (pixVal < threshold) {
+       return pmFootprintAlloc(0, img);
+   }
+   
+   pmFootprint *fp = pmFootprintAlloc(1 + img->numRows/10, img);
+/*
+ * We need a mask for two purposes; to indicate which pixels are already detected,
+ * and to store the "stop" pixels --- those that, once reached, should stop us
+ * looking for the rest of the pmFootprint.  These are generally set from peaks.
+ */
+   psImage *mask = psImageAlloc(numCols, numRows, PS_TYPE_MASK);
+   P_PSIMAGE_SET_ROW0(mask, row0);
+   P_PSIMAGE_SET_COL0(mask, col0);
+   psImageInit(mask, PM_SSPAN_INITIAL);
+   //
+   // Set stop bits from peaks list
+   //
+   assert (peaks == NULL || peaks->n == 0 || pmPeakTest(peaks->data[0]));
+   if (peaks != NULL) {
+       for (int i = 0; i < peaks->n; i++) {
+	   pmPeak *peak = peaks->data[i];
+	   mask->data.PS_TYPE_MASK_DATA[peak->y - mask->row0][peak->x - mask->col0] |= PM_SSPAN_STOP;
+       }
+   }
+/*
+ * Find starting span passing through (row, col)
+ */
+   psArray *startspans = psArrayAllocEmpty(1); // spans where we have to restart the search
+   
+   imgRowF32 = img->data.F32[row];	// only one of
+   imgRowS32 = img->data.S32[row];	//      these is valid!
+   psMaskType *maskRow = mask->data.PS_TYPE_MASK_DATA[row];
+   {
+       int i;
+       for (i = col; i >= 0; i--) {
+	   pixVal = F32 ? imgRowF32[i] : imgRowS32[i];
+	   if ((maskRow[i] & PM_SSPAN_DETECTED) || pixVal < threshold) {
+	       break;
+	   }
+       }
+       int i0 = i;
+       for (i = col; i < numCols; i++) {
+	   pixVal = F32 ? imgRowF32[i] : imgRowS32[i];
+	   if ((maskRow[i] & PM_SSPAN_DETECTED) || pixVal < threshold) {
+	       break;
+	   }
+       }
+       int i1 = i;
+       const pmSpan *sp = pmFootprintAddSpan(fp, row + row0, i0 + col0 + 1, i1 + col0 - 1);
+
+       (void)add_startspan(startspans, sp, mask, PM_SSPAN_RESTART);
+   }
+   /*
+    * Now workout from those Spartspans, searching for pixels above threshold
+    */
+   while (do_startspan(fp, img, mask, threshold, startspans)) continue;
+   /*
+    * Cleanup
+    */
+   psFree(mask);
+   psFree(startspans);			// restores the image pixel
+
+   return fp;				// pmFootprint really
+}
+
+/************************************************************************************************************/
+/*
+ * Worker routine for the pmSetFootprintArrayIDs/pmSetFootprintID (and pmMergeFootprintArrays)
+ */
+static void
+set_footprint_id(psImage *idImage,	// the image to set
+		 const pmFootprint *fp, // the footprint to insert
+		 const int id) {	// the desired ID
+   const int col0 = fp->region.x0;
+   const int row0 = fp->region.y0;
+
+   for (int j = 0; j < fp->spans->n; j++) {
+       const pmSpan *span = fp->spans->data[j];
+       psS32 *imgRow = idImage->data.S32[span->y - row0];
+       for(int k = span->x0 - col0; k <= span->x1 - col0; k++) {
+	   imgRow[k] += id;
+       }
+   }
+}
+
+static void
+set_footprint_array_ids(psImage *idImage,
+			const psArray *footprints, // the footprints to insert
+			const bool relativeIDs) { // show IDs starting at 0, not pmFootprint->id
+   int id = 0;				// first index will be 1
+   for (int i = 0; i < footprints->n; i++) {
+       const pmFootprint *fp = footprints->data[i];
+       if (relativeIDs) {
+	   id++;
+       } else {
+	   id = fp->id;
+       }
+       
+       set_footprint_id(idImage, fp, id);
+   }
+}
+
+/*
+ * Set an image to the value of footprint's ID whever they may fall
+ */
+psImage *pmSetFootprintArrayIDs(const psArray *footprints, // the footprints to insert
+				const bool relativeIDs) { // show IDs starting at 1, not pmFootprint->id
+   assert (footprints != NULL);
+
+   if (footprints->n == 0) {
+       psError(PS_ERR_BAD_PARAMETER_SIZE, true, "You didn't provide any footprints");
+       return NULL;
+   }
+   const pmFootprint *fp = footprints->data[0];
+   assert(pmFootprintTest((const psPtr)fp));
+   const int numCols = fp->region.x1 - fp->region.x0 + 1;
+   const int numRows = fp->region.y1 - fp->region.y0 + 1;
+   const int col0 = fp->region.x0;
+   const int row0 = fp->region.y0;
+   assert (numCols >= 0 && numRows >= 0);
+   
+   psImage *idImage = psImageAlloc(numCols, numRows, PS_TYPE_S32);
+   P_PSIMAGE_SET_ROW0(idImage, row0);
+   P_PSIMAGE_SET_COL0(idImage, col0);
+   psImageInit(idImage, 0);
+   /*
+    * do the work
+    */
+   set_footprint_array_ids(idImage, footprints, relativeIDs);
+
+   return idImage;
+   
+}
+
+/*
+ * Set an image to the value of footprint's ID whever they may fall
+ */
+psImage *pmSetFootprintID(const pmFootprint *fp, // the footprint to insert
+			  const int id) {	// the desired ID
+   assert(fp != NULL && pmFootprintTest((const psPtr)fp));
+   const int numCols = fp->region.x1 - fp->region.x0 + 1;
+   const int numRows = fp->region.y1 - fp->region.y0 + 1;
+   const int col0 = fp->region.x0;
+   const int row0 = fp->region.y0;
+   assert (numCols >= 0 && numRows >= 0);
+   
+   psImage *idImage = psImageAlloc(numCols, numRows, PS_TYPE_S32);
+   P_PSIMAGE_SET_ROW0(idImage, row0);
+   P_PSIMAGE_SET_COL0(idImage, col0);
+   psImageInit(idImage, 0);
+   /*
+    * do the work
+    */
+   set_footprint_id(idImage, fp, id);
+
+   return idImage;
+   
+}
+
+/************************************************************************************************************/
+/*
+ * Grow a psArray of pmFootprints isotropically by r pixels, returning a new psArray of new pmFootprints
+ */
+psArray *pmGrowFootprintArray(const psArray *footprints, // footprints to grow
+			      int r) {	// how much to grow each footprint
+    assert (footprints->n == 0 || pmFootprintTest(footprints->data[0]));
+
+    if (footprints->n == 0) {		// we don't know the size of the footprint's region
+	return psArrayAlloc(0);
+    }
+    /*
+     * We'll insert the footprints into an image, then convolve with a disk,
+     * then extract a footprint from the result --- this is magically what we want.
+     */
+    psImage *idImage = pmSetFootprintArrayIDs(footprints, true);
+    if (r <= 0) {
+	r = 1;				// r == 1 => no grow
+    }
+    psKernel *circle = psKernelAlloc(-r, r, -r, r);
+    assert (circle->image->numRows == 2*r + 1 && circle->image->numCols == circle->image->numRows);
+    for (int i = 0; i <= r; i++) {
+	for (int j = 0; j <= r; j++) {
+	    if (i*i + j*j <= r*r) {
+		circle->kernel[i][j] = 
+		    circle->kernel[i][-j] = 
+		    circle->kernel[-i][j] = 
+		    circle->kernel[-i][-j] = 1;
+	    }
+	}
+    }
+
+    psImage *grownIdImage = psImageConvolveDirect(NULL, idImage, circle); // Here's the actual grow step
+    psFree(circle);	
+
+    psArray *grown = pmFindFootprints(grownIdImage, 0.5, 1); // and here we rebuild the grown footprints
+    assert (grown != NULL);
+    psFree(idImage); psFree(grownIdImage);
+    /*
+     * Now assign the peaks appropriately.  We could do this more efficiently
+     * using grownIdImage (which we just freed), but this is easy and probably fast enough
+     */
+    const psArray *peaks = pmFootprintArrayToPeaks(footprints);
+    pmPeaksAssignToFootprints(grown, peaks);
+    psFree((psArray *)peaks);
+
+    return grown;
+    
+}
+
+/************************************************************************************************************/
+/*
+ * Merge together two psArrays of pmFootprints neither of which is damaged.
+ *
+ * The returned psArray may contain elements of the inital psArrays (with
+ * their reference counters suitable incremented)
+ */
+psArray *pmMergeFootprintArrays(const psArray *footprints1, // one set of footprints
+				const psArray *footprints2, // the other set
+				const int includePeaks) { // which peaks to set? 0x1 => footprints1, 0x2 => 2
+    assert (footprints1->n == 0 || pmFootprintTest(footprints1->data[0]));
+    assert (footprints2->n == 0 || pmFootprintTest(footprints2->data[0]));
+
+    if (footprints1->n == 0 || footprints2->n == 0) {		// nothing to do but put copies on merged
+	const psArray *old = (footprints1->n == 0) ? footprints2 : footprints1;
+
+	psArray *merged = psArrayAllocEmpty(old->n);
+	for (int i = 0; i < old->n; i++) {
+	    psArrayAdd(merged, 1, old->data[i]);
+	}
+	
+	return merged;
+    }
+    /*
+     * We have real work to do as some pmFootprints in footprints2 may overlap
+     * with footprints1
+     */
+    {
+	pmFootprint *fp1 = footprints1->data[0];
+	pmFootprint *fp2 = footprints2->data[0];
+	if (fp1->region.x0 != fp2->region.x0 ||
+	    fp1->region.x1 != fp2->region.x1 ||
+	    fp1->region.y0 != fp2->region.y0 ||
+	    fp1->region.y1 != fp2->region.y1) {
+	    psError(PS_ERR_BAD_PARAMETER_SIZE, true,
+		    "The two pmFootprint arrays correspnond to different-sized regions");
+	    return NULL;
+	}
+    }
+    /*
+     * We'll insert first one set of footprints then the other into an image, then
+     * extract a footprint from the result --- this is magically what we want.
+     */
+    psImage *idImage = pmSetFootprintArrayIDs(footprints1, true);
+    set_footprint_array_ids(idImage, footprints2, true);
+
+    psArray *merged = pmFindFootprints(idImage, 0.5, 1);
+    assert (merged != NULL);
+    psFree(idImage);
+    /*
+     * Now assign the peaks appropriately.  We could do this more efficiently
+     * using idImage (which we just freed), but this is easy and probably fast enough
+     */ 
+    if (includePeaks & 0x1) {
+	const psArray *peaks = pmFootprintArrayToPeaks(footprints1);
+	pmPeaksAssignToFootprints(merged, peaks);
+	psFree((psArray *)peaks);
+    }
+
+    if (includePeaks & 0x2) {
+	const psArray *peaks = pmFootprintArrayToPeaks(footprints2);
+	pmPeaksAssignToFootprints(merged, peaks);
+	psFree((psArray *)peaks);
+    }
+    
+    return merged;
+}
+
+/************************************************************************************************************/
+/*
+ * Given a psArray of pmFootprints and another of pmPeaks, assign the peaks to the
+ * footprints in which that fall; if they _don't_ fall in a footprint, add a suitable
+ * one to the list.
+ */
+psErrorCode
+pmPeaksAssignToFootprints(psArray *footprints,	// the pmFootprints
+			  const psArray *peaks) { // the pmPeaks
+    assert (footprints != NULL);
+    assert (footprints->n == 0 || pmFootprintTest(footprints->data[0]));
+    assert (peaks != NULL);
+    assert (peaks->n == 0 || pmPeakTest(peaks->data[0]));
+    
+    if (footprints->n == 0) {
+	if (peaks->n > 0) {
+	    return psError(PS_ERR_BAD_PARAMETER_SIZE, true, "Your list of footprints is empty");
+	}
+	return PS_ERR_NONE;
+    }
+    /*
+     * Create an image filled with the object IDs, and use it to assign pmPeaks to the
+     * objects
+     */
+    psImage *ids = pmSetFootprintArrayIDs(footprints, true);
+    assert (ids != NULL);
+    assert (ids->type.type == PS_TYPE_S32);
+    const int row0 = ids->row0;
+    const int col0 = ids->col0;
+    const int numRows = ids->numRows;
+    const int numCols = ids->numCols;
+
+    for (int i = 0; i < peaks->n; i++) {
+	pmPeak *peak = peaks->data[i];
+	const int x = peak->x - col0;
+	const int y = peak->y - row0;
+	
+	assert (x >= 0 && x < numCols && y >= 0 && y < numRows);
+	int id = ids->data.S32[y][x - col0];
+
+	if (id == 0) {			// peak isn't in a footprint, so make one for it
+	    pmFootprint *nfp = pmFootprintAlloc(1, ids);
+	    pmFootprintAddSpan(nfp, y, x, x);
+	    psArrayAdd(footprints, 1, nfp);
+	    psFree(nfp);
+	    id = footprints->n;
+	}
+
+	assert (id >= 1 && id <= footprints->n);
+	pmFootprint *fp = footprints->data[id - 1];
+	psArrayAdd(fp->peaks, 5, peak);
+    }
+    
+    psFree(ids);
+    //
+    // Make sure that peaks within each footprint are sorted and unique
+    //
+    for (int i = 0; i < footprints->n; i++) {
+	pmFootprint *fp = footprints->data[i];
+        fp->peaks = psArraySort(fp->peaks, pmPeakSortBySN);
+
+	for (int j = 1; j < fp->peaks->n; j++) { // check for duplicates
+	    if (fp->peaks->data[j] == fp->peaks->data[j-1]) {
+		(void)psArrayRemoveIndex(fp->peaks, j);
+		j--;			// we moved everything down one
+	    }
+	}
+    }
+
+    return PS_ERR_NONE;
+}
+
+/************************************************************************************************************/
+ /*
+  * Examine the peaks in a pmFootprint, and throw away the ones that are not sufficiently
+  * isolated.  More precisely, for each peak find the highest coll that you'd have to traverse
+  * to reach a still higher peak --- and if that coll's more than nsigma DN below your
+  * starting point, discard the peak.
+  */
+psErrorCode pmFootprintCullPeaks(const psImage *img, // the image wherein lives the footprint
+				 const psImage *weight,	// corresponding variance image
+				 pmFootprint *fp, // Footprint containing mortal peaks
+				 const float nsigma_delta, // how many sigma above local background a peak
+				 	// needs to be to survive
+				 const float min_threshold) { // minimum permitted coll height
+    assert (img != NULL); assert (img->type.type == PS_TYPE_F32);
+    assert (weight != NULL); assert (weight->type.type == PS_TYPE_F32);
+    assert (img->row0 == weight->row0 && img->col0 == weight->col0);
+    assert (fp != NULL);
+
+    if (fp->peaks == NULL || fp->peaks->n == 0) { // nothing to do
+	return PS_ERR_NONE;
+    }
+
+    psRegion subRegion;			// desired subregion; 1 larger than bounding box (grr)
+    subRegion.x0 = fp->bbox.x0; subRegion.x1 = fp->bbox.x1 + 1;
+    subRegion.y0 = fp->bbox.y0; subRegion.y1 = fp->bbox.y1 + 1;
+    const psImage *subImg = psImageSubset((psImage *)img, subRegion);
+    const psImage *subWt = psImageSubset((psImage *)weight, subRegion);
+    assert (subImg != NULL && subWt != NULL);
+    //
+    // We need a psArray of peaks brighter than the current peak.  We'll fake this
+    // by reusing the fp->peaks but lying about n.
+    //
+    // We do this for efficiency (otherwise I'd need two peaks lists), and we are
+    // rather too chummy with psArray in consequence.  But it works.
+    //
+    psArray *brightPeaks = psArrayAlloc(0);
+    psFree(brightPeaks->data);
+    brightPeaks->data = psMemIncrRefCounter(fp->peaks->data);// use the data from fp->peaks
+    //
+    // The brightest peak is always safe; go through other peaks trying to cull them
+    //
+    for (int i = 1; i < fp->peaks->n; i++) { // n.b. fp->peaks->n can change within the loop
+	const pmPeak *peak = fp->peaks->data[i];
+	int x = peak->x - subImg->col0;
+	int y = peak->y - subImg->row0;
+	//
+	// Find the level nsigma below the peak that must separate the peak
+	// from any of its friends
+	//
+	assert (x >= 0 && x < subImg->numCols && y >= 0 && y < subImg->numRows);
+	const float stdev = sqrt(subWt->data.F32[y][x]);
+	float threshold = subImg->data.F32[y][x] - nsigma_delta*stdev;
+	if (isnan(threshold) || threshold < min_threshold) {
+#if 1					// min_threshold is assumed to be below the detection threshold,
+					// so all the peaks are pmFootprint, and this isn't the brightest
+	    (void)psArrayRemoveIndex(fp->peaks, i);
+	    i--;			// we moved everything down one
+	    continue;
+#else
+#error n.b. We will be running LOTS of checks at this threshold, so only find the footprint once
+	    threshold = min_threshold;
+#endif
+	}
+	if (threshold > subImg->data.F32[y][x]) {
+	    threshold = subImg->data.F32[y][x] - 10*FLT_EPSILON;
+	}
+
+	const int peak_id = 1;		// the ID for the peak of interest
+	brightPeaks->n = i;		// only stop at a peak brighter than we are
+	pmFootprint *peakFootprint = pmFindFootprintAtPoint(subImg, threshold, brightPeaks, peak->y, peak->x);
+	brightPeaks->n = 0;		// don't double free
+	psImage *idImg = pmSetFootprintID(peakFootprint, peak_id);
+	psFree(peakFootprint);
+
+	int j;
+	for (j = 0; j < i; j++) {
+	    const pmPeak *peak2 = fp->peaks->data[j];
+	    int x2 = peak2->x - subImg->col0;
+	    int y2 = peak2->y - subImg->row0;
+	    const int peak2_id = idImg->data.S32[y2][x2]; // the ID for some other peak
+
+	    if (peak2_id == peak_id) {	// There's a brighter peak within the footprint above
+		;			// threshold; so cull our initial peak
+		(void)psArrayRemoveIndex(fp->peaks, i);
+		i--;			// we moved everything down one
+		break;
+	    }
+	}
+	if (j == i) {
+	    j++;
+	}
+
+	psFree(idImg);
+    }
+
+    brightPeaks->n = 0; psFree(brightPeaks);
+    psFree((psImage *)subImg);
+    psFree((psImage *)subWt);
+
+    return PS_ERR_NONE;
+}
+
+/*
+ * Cull an entire psArray of pmFootprints
+ */
+psErrorCode
+pmFootprintArrayCullPeaks(const psImage *img, // the image wherein lives the footprint
+			  const psImage *weight,	// corresponding variance image
+			  psArray *footprints, // array of pmFootprints
+			  const float nsigma_delta, // how many sigma above local background a peak
+    					// needs to be to survive
+			  const float min_threshold) { // minimum permitted coll height
+    for (int i = 0; i < footprints->n; i++) {
+	pmFootprint *fp = footprints->data[i];
+	if (pmFootprintCullPeaks(img, weight, fp, nsigma_delta, min_threshold) != PS_ERR_NONE) {
+	    return psError(PS_ERR_UNKNOWN, false, "Culling pmFootprint %d", fp->id);
+	}
+    }
+    
+    return PS_ERR_NONE;
+}
+
+/************************************************************************************************************/
+/*
+ * Extract the peaks in a psArray of pmFootprints, returning a psArray of pmPeaks
+ */
+psArray *pmFootprintArrayToPeaks(const psArray *footprints) {
+   assert(footprints != NULL);
+   assert(footprints->n == 0 || pmFootprintTest(footprints->data[0]));
+
+   int npeak = 0;
+   for (int i = 0; i < footprints->n; i++) {
+      const pmFootprint *fp = footprints->data[i];
+      npeak += fp->peaks->n;
+   }
+
+   psArray *peaks = psArrayAllocEmpty(npeak);
+   
+   for (int i = 0; i < footprints->n; i++) {
+      const pmFootprint *fp = footprints->data[i];
+      for (int j = 0; j < fp->peaks->n; j++) {
+	 psArrayAdd(peaks, 1, fp->peaks->data[j]);
+      }
+   }
+
+   return peaks;
+}
Index: /branches/eam_branch_20071023/psphot/src/pmFootprint.h
===================================================================
--- /branches/eam_branch_20071023/psphot/src/pmFootprint.h	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/pmFootprint.h	(revision 15358)
@@ -0,0 +1,54 @@
+#if !defined(PM_FOOTPRINT_H)
+#define PM_FOOTPRINT_H
+//
+// Describe a segment of an image
+//
+typedef struct {
+    int y;                              //!< Row that span's in
+    int x0;                             //!< Starting column (inclusive)
+    int x1;                             //!< Ending column (inclusive)
+} pmSpan;
+
+pmSpan *pmSpanAlloc(int y, int x1, int x2);
+bool pmSpanTest(const psPtr ptr);
+int pmSpanSortByYX (const void **a, const void **b);
+
+typedef struct {
+    const int id;                       //!< unique ID
+    int npix;                           //!< number of pixels in this pmFootprint
+    psArray *spans;                     //!< the pmSpans
+    psRegion bbox;                      //!< the pmFootprint's bounding box
+    psArray *peaks;                     //!< the peaks lying in this footprint
+    psRegion region;   //!< A region describing the psImage the footprints live in
+    bool normalized;                    //!< Are the spans sorted? 
+} pmFootprint;
+
+pmFootprint *pmFootprintAlloc(int nspan, const psImage *img);
+bool pmFootprintTest(const psPtr ptr);
+
+pmFootprint *pmFootprintNormalize(pmFootprint *fp);
+int pmFootprintSetNpix(pmFootprint *fp);
+void pmFootprintSetBBox(pmFootprint *fp);
+psArray *pmFindFootprints(const psImage *img, const float threshold, const int npixMin);
+pmFootprint *pmFindFootprintAtPoint(const psImage *img,
+                                    const float threshold,
+                                    const psArray *peaks,
+                                    int row, int col);
+
+psArray *pmGrowFootprintArray(const psArray *footprints, int r);
+psArray *pmMergeFootprintArrays(const psArray *footprints1, const psArray *footprints2,
+                                const int includePeaks);
+
+psImage *pmSetFootprintArrayIDs(const psArray *footprints, const bool relativeIDs);
+psImage *pmSetFootprintID(const pmFootprint *fp, const int id);
+
+psErrorCode pmPeaksAssignToFootprints(psArray *footprints, const psArray *peaks);
+
+psErrorCode pmFootprintArrayCullPeaks(const psImage *img, const psImage *weight, psArray *footprints,
+                                 const float nsigma, const float threshold_min);
+psErrorCode pmFootprintCullPeaks(const psImage *img, const psImage *weight, pmFootprint *fp,
+                                 const float nsigma, const float threshold_min);
+
+psArray *pmFootprintArrayToPeaks(const psArray *footprints);
+
+#endif
Index: /branches/eam_branch_20071023/psphot/src/psphot.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphot.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphot.c	(revision 15358)
@@ -0,0 +1,42 @@
+# include "psphotStandAlone.h"
+
+static void usage (void) {
+    fprintf (stderr, "USAGE: psphot [-file image(s)] [-list imagelist] (output)\n");
+    exit (PS_EXIT_CONFIG_ERROR);
+}
+
+int main (int argc, char **argv) {
+
+    psTimerStart ("complete");
+    pmErrorRegister();			// register psModule's error codes/messages
+    psphotErrorRegister();              // register our error codes/messages
+    psphotModelClassInit ();            // load implementation-specific models
+
+    // load command-line arguments, options, and system config data
+    pmConfig *config = psphotArguments (argc, argv);
+    if (!config) {
+	psErrorStackPrint(stderr, "Error reading arguments\n");
+	usage ();
+    }
+
+    // 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 (!psphotImageLoop (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/eam_branch_20071023/psphot/src/psphot.h
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphot.h	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphot.h	(revision 15358)
@@ -0,0 +1,171 @@
+/* This file defines the library functions available to external programs.  It must be included
+ * by programs which are compiled against psphot functions.
+ */
+
+#ifndef PSPHOT_H
+#define PSPHOT_H
+
+#include <psmodules.h>
+
+#include "psphotErrorCodes.h"
+#define PSPHOT_RECIPE "PSPHOT" // Name of the recipe to use
+
+// top-level psphot functions
+const char     *psphotCVSName(void);
+psString        psphotVersion(void);
+psString        psphotVersionLong(void);
+
+bool            psphotModelTest (pmConfig *config, const pmFPAview *view, psMetadata *recipe, psMaskType maskVal, psMaskType mark);
+bool            psphotReadout (pmConfig *config, const pmFPAview *view);
+bool            psphotReadoutCleanup (pmConfig *config, pmReadout *readout, psMetadata *recipe, pmPSF *psf, psArray *sources);
+bool            psphotDefineFiles (pmConfig *config, pmFPAfile *input);
+
+
+// XXX test functions
+psArray        *psphotFakeSources ();
+
+// psphotReadout functions
+bool            psphotImageMedian (pmConfig *config, const pmFPAview *view, psMaskType maskVal);
+psArray        *psphotFindPeaks (pmReadout *readout, psMetadata *recipe,
+                                 const bool returnFootprints, const int pass, psMaskType maskVal);
+#include "pmFootprint.h"
+psErrorCode     psphotCullPeaks(const psImage *img, const psImage *weight,
+                                const psMetadata *recipe, psArray *footprints);
+psArray        *psphotSourceStats (pmReadout *readout, psMetadata *recipe, psArray *allpeaks, psMaskType maskVal, psMaskType mark);
+bool            psphotRoughClass (psArray *sources, psMetadata *recipe, const bool findPsfClump, psMaskType maskSat);
+bool            psphotBasicDeblend (psArray *sources, psMetadata *recipe);
+pmPSF          *psphotChoosePSF (pmReadout *readout, psArray *sources, psMetadata *recipe, psMaskType maskVal, psMaskType mark);
+bool            psphotPSFstats (pmReadout *readout, psMetadata *recipe, pmPSF *psf);
+bool            psphotMomentsStats (pmReadout *readout, psMetadata *recipe, psArray *sources);
+#if NOT_IN_LIBPSPHOT
+bool            psphotEnsemblePSF (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, bool final);
+#endif
+bool            psphotFitSourcesLinear (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, bool final, psMaskType maskVal);
+bool            psphotGuessModels (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, psMaskType maskVal);
+bool            psphotBlendFit (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, psMaskType maskVal);
+bool            psphotReplaceUnfit (psArray *sources, psMaskType maskVal);
+bool            psphotReplaceAll (psArray *sources, psMaskType maskVal);
+bool            psphotApResid (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, psMaskType maskVal, psMaskType mark);
+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            psphotMagnitudes (psArray *sources, psMetadata *recipe, pmPSF *psf, pmReadout *background, psMaskType maskVal, psMaskType mark);
+bool            psphotSkyReplace (pmConfig *config, const pmFPAview *view);
+
+// basic support functions
+void            psphotModelClassInit (void);
+bool            psphotGrowthCurve (pmReadout *readout, pmPSF *psf, bool ignore, psMaskType maskVal);
+bool            psphotMaskReadout (pmReadout *readout, psMetadata *recipe, psMaskType maskVal);
+void            psphotSourceFreePixels (psArray *sources);
+
+// functions to set the correct source pixels
+bool            psphotInitRadiusPSF (const psMetadata *recipe, const pmModelType type);
+bool            psphotCheckRadiusPSF (pmReadout *readout, pmSource *source, pmModel *model);
+bool            psphotCheckRadiusPSFBlend (pmReadout *readout, pmSource *source, pmModel *model, float dR);
+bool            psphotInitRadiusEXT (psMetadata *recipe, pmModelType type);
+bool            psphotCheckRadiusEXT (pmReadout *readout, pmSource *source, pmModel *model);
+
+// output functions
+bool            psphotAddPhotcode (psMetadata *recipe, pmConfig *config, const pmFPAview *view);
+bool            psphotDumpMoments (psMetadata *recipe, psArray *sources);
+psMetadata     *psphotDefineHeader (psMetadata *recipe);
+int             psphotSaveImage (psMetadata *header, psImage *image, char *filename);
+bool            psphotDumpConfig (pmConfig *config);
+pmReadout      *psphotSelectBackground (pmConfig *config, const pmFPAview *view, const bool stdev);
+
+// PSF / DBL / EXT evaluation functions
+bool            psphotEvalPSF (pmSource *source, pmModel *model);
+bool            psphotEvalDBL (pmSource *source, pmModel *model);
+bool            psphotEvalEXT (pmSource *source, pmModel *model);
+
+//  functions to support the source fitting process
+bool            psphotInitLimitsPSF (psMetadata *recipe, pmReadout *readout);
+bool            psphotInitLimitsEXT (psMetadata *recipe);
+bool            psphotFitBlend (pmReadout *readout, pmSource *source, pmPSF *psf, psMaskType maskVal);
+bool            psphotFitBlob (pmReadout *readout, pmSource *source, psArray *sources, pmPSF *psf, psMaskType maskVal);
+bool            psphotFitPSF (pmReadout *readout, pmSource *source, pmPSF *psf, psMaskType maskVal);
+pmModel        *psphotFitEXT (pmReadout *readout, pmSource *source, psMaskType maskVal);
+psArray        *psphotFitDBL (pmReadout *readout, pmSource *source, psMaskType maskVal);
+
+// functions to support simultaneous multi-source fitting
+bool            psphotFitSet (pmSource *oneSrc, pmModel *oneModel, char *fitset, pmSourceFitMode mode, psMaskType maskVal);
+
+// plotting functions (available if libkapa is installed)
+bool            psphotPlotMoments (pmConfig *config, pmFPAview *view, psArray *sources);
+bool            psphotPlotPSFModel (pmConfig *config, pmFPAview *view, psArray *sources);
+bool            psphotFitInit ();
+bool            psphotFitSummary ();
+bool            psphotMergeSources (psArray *oldSources, psArray *newSources);
+bool            psphotLoadExtSources (pmConfig *config, const pmFPAview *view, psArray *sources);
+pmPSF          *psphotLoadPSF (pmConfig *config, const pmFPAview *view, psMetadata *recipe);
+bool            psphotSetHeaderNstars (psMetadata *recipe, psArray *sources);
+bool            psphotAddNoise (pmReadout *readout, psArray *sources, psMetadata *recipe, bool add, psMaskType maskVal);
+bool            psphotRadialPlot (int *kapa, const char *filename, pmSource *source);
+bool            psphotSourcePlots (pmReadout *readout, psArray *sources, psMetadata *recipe, psMaskType maskVal);
+bool            psphotMosaicSubimage (psImage *outImage, pmSource *source, int Xo, int Yo, int DX, int DY);
+
+bool            psphotAddWithTest (pmSource *source, bool useState, psMaskType maskVal);
+bool            psphotSubWithTest (pmSource *source, bool useState, psMaskType maskVal);
+bool            psphotSetState (pmSource *source, bool curState, psMaskType maskVal);
+bool            psphotDeblendSatstars (psArray *sources, psMetadata *recipe);
+bool            psphotSourceSize (pmReadout *readout, psArray *sources, psMetadata *recipe);
+
+bool            psphotMakeResiduals (psArray *sources, psMetadata *recipe, pmPSF *psf, psMaskType maskVal);
+
+bool            psphotExtendedSources (pmReadout *readout, psArray *sources, psMetadata *recipe, psMaskType maskVal);
+bool            psphotPSFConvModel (pmSource *source, psMetadata *recipe, psMaskType maskVal);
+psKernel       *psphotKernelFromPSF (pmSource *source, int nPix);
+
+bool 		psphotPetrosian (pmSource *source, psMetadata *recipe, psMaskType maskVal);
+bool 		psphotIsophotal (pmSource *source, psMetadata *recipe, psMaskType maskVal);
+bool 		psphotAnnuli (pmSource *source, psMetadata *recipe, psMaskType maskVal);
+bool 		psphotKron (pmSource *source, psMetadata *recipe, psMaskType maskVal);
+
+// 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
+bool psphotModelWithPSF_LMM (
+    psMinimization *min,
+    psImage *covar,
+    psVector *params,
+    psMinConstraint *constraint,
+    pmSource *source,
+    const psKernel *psf,
+    psMinimizeLMChi2Func func);
+
+psF32 psphotModelWithPSF_SetABX(
+    psImage  *alpha,
+    psVector *beta,
+    const psVector *params,
+    const psVector *paramMask,
+    pmPCMData *pcm,
+    const pmSource *source,
+    const psKernel *psf,
+    psMinimizeLMChi2Func func);
+
+pmPCMData *pmPCMDataAlloc (
+    const psVector *params,
+    const psVector *paramMask,
+    pmSource *source);
+
+psImage *pmPCMDataSaveImage (pmPCMData *pcm);
+
+int psphotKapaOpen ();
+bool psphotKapaClose ();
+bool psphotImageBackgroundCellHistogram (psVector *values, float mean, float sigma, int ix, int iy);
+bool psphotDiagnosticPlots (pmConfig *config, char *name, ...);
+
+bool psphotMakeFluxScale (psImage *image, psMetadata *recipe, pmPSF *psf);
+
+bool psphotCheckStarDistribution (psArray *sources, psArray *stars, pmPSFOptions *options);
+int psphotSupplementStars (psArray *stars, psArray *sources, psImageBinning *binning, int ix, int iy);
+
+#endif
Index: /branches/eam_branch_20071023/psphot/src/psphotAddNoise.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotAddNoise.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotAddNoise.c	(revision 15358)
@@ -0,0 +1,84 @@
+# include "psphotInternal.h"
+
+bool psphotAddNoise (pmReadout *readout, psArray *sources, psMetadata *recipe, bool add, psMaskType maskVal) {
+
+    bool status = false;
+    psEllipseShape oldshape;
+    psEllipseShape newshape;
+    psEllipseAxes axes;
+
+    PS_ASSERT (readout, false);
+    PS_ASSERT (readout->parent, false);
+    PS_ASSERT (readout->parent->concepts, false);
+
+    psTimerStart ("psphot");
+
+    // increase variance by factor*(object noise):
+    // weight = flux/gain + rn^2/g^2
+    // we are adding factor*flux/gain
+
+    float FACTOR = psMetadataLookupF32 (&status, recipe, "NOISE.FACTOR");
+    PS_ASSERT (status, false);
+    float SIZE = psMetadataLookupF32 (&status, recipe, "NOISE.SIZE");
+    PS_ASSERT (status, false);
+
+    if (SIZE <= 0) {
+       return true;
+    }
+
+    float GAIN = psMetadataLookupF32(&status, readout->parent->concepts, "CELL.GAIN"); // Cell gain
+    PS_ASSERT (status, false);
+
+    FACTOR /= GAIN;
+
+    // loop over all source
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+
+        // skip sources which were not subtracted
+        if (!(source->mode & PM_SOURCE_MODE_SUBTRACTED)) continue;
+
+        // select appropriate model
+        pmModel *model = pmSourceGetModel (NULL, source);
+        if (model == NULL) continue;  // model must be defined
+
+        if (add) {
+            psTrace ("psphot", 4, "adding noise for object at %f,%f\n", model->params->data.F32[PM_PAR_XPOS], model->params->data.F32[PM_PAR_YPOS]);
+        } else {
+            psTrace ("psphot", 4, "remove noise for object at %f,%f\n", model->params->data.F32[PM_PAR_XPOS], model->params->data.F32[PM_PAR_YPOS]);
+        }
+
+        psF32 *PAR = model->params->data.F32;
+
+        // save original values
+        float oldI0  = PAR[PM_PAR_I0];
+        oldshape.sx  = PAR[PM_PAR_SXX];
+        oldshape.sy  = PAR[PM_PAR_SYY];
+        oldshape.sxy = PAR[PM_PAR_SXY];
+
+        // increase size and height of source
+        axes = psEllipseShapeToAxes (oldshape, 20.0);
+        axes.major *= SIZE;
+        axes.minor *= SIZE;
+        newshape = psEllipseAxesToShape (axes);
+        PAR[PM_PAR_I0]  = FACTOR*PS_SQR(oldI0);
+        PAR[PM_PAR_SXX] = newshape.sx;
+        PAR[PM_PAR_SYY] = newshape.sy;
+        PAR[PM_PAR_SXY] = newshape.sxy;
+
+        // XXX if we use pmSourceOp, the size (and possibly Io) will not be respected
+        pmSourceOp (source, PM_MODEL_OP_FULL | PM_MODEL_OP_NOISE, add, maskVal, 0, 0);
+
+        // restore original values
+        PAR[PM_PAR_I0]  = oldI0;
+        PAR[PM_PAR_SXX] = oldshape.sx;
+        PAR[PM_PAR_SYY] = oldshape.sy;
+        PAR[PM_PAR_SXY] = oldshape.sxy;
+    }
+    if (add) {
+        psLogMsg ("psphot.noise", PS_LOG_INFO, "add noise for %ld objects: %f sec\n", sources->n, psTimerMark ("psphot"));
+    } else {
+        psLogMsg ("psphot.noise", PS_LOG_INFO, "sub noise for %ld objects: %f sec\n", sources->n, psTimerMark ("psphot"));
+    }
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotAnnuli.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotAnnuli.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotAnnuli.c	(revision 15358)
@@ -0,0 +1,8 @@
+# include "psphot.h"
+
+bool psphotAnnuli (pmSource *source, psMetadata *recipe, psMaskType maskVal) {
+
+  psLogMsg ("psphot", PS_LOG_INFO, "not implemented\n");
+  return true;
+
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotApResid.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotApResid.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotApResid.c	(revision 15358)
@@ -0,0 +1,340 @@
+# include "psphotInternal.h"
+
+# define SKIPSTAR(MSG) { psTrace ("psphot", 3, "invalid : %s", MSG); continue; }
+// measure the aperture residual statistics and 2D variations
+bool psphotApResid (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, psMaskType maskVal, psMaskType mark)
+{
+    int Nfail = 0;
+    int Nskip = 0;
+    int Npsf;
+    bool status;
+    pmModel *model;
+    pmSource *source;
+
+    PS_ASSERT_PTR_NON_NULL(psf, false);
+    // XXX drop this? PS_ASSERT_PTR_NON_NULL(psf->ApTrend, false);
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(recipe, false);
+
+    psTimerStart ("psphot");
+
+    // S/N limit to perform full non-linear fits
+    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...
+    float PSF_FIT_PAD   = psMetadataLookupF32 (&status, recipe, "PSF_FIT_PADDING");
+    bool IGNORE_GROWTH = psMetadataLookupBool (&status, recipe, "IGNORE_GROWTH");
+    bool INTERPOLATE_AP = psMetadataLookupBool (&status, recipe, "INTERPOLATE_AP");
+
+    // XXX is this still needed?  the pmTrend2D stuff should be auto-adjusting...
+    int APTREND_NSTAR_MIN = psMetadataLookupS32(&status, recipe, "APTREND.NSTAR.MIN");
+    assert (status);
+
+    // maximum order for aperture correction
+    int APTREND_ORDER_MAX = psMetadataLookupS32(&status, recipe, "APTREND.ORDER.MAX");
+    assert (status);
+
+    if (APTREND_ORDER_MAX < 1) {
+        psError(PSPHOT_ERR_CONFIG, true, "APTREND.ORDER.MAX must be 1 or more");
+        return false;
+    }
+
+    pmSourcePhotometryMode photMode = 0;
+    if (!IGNORE_GROWTH) photMode |= PM_SOURCE_PHOT_GROWTH;
+    if (INTERPOLATE_AP) photMode |= PM_SOURCE_PHOT_INTERP;
+
+    // set limits on the aperture magnitudes
+    pmSourceMagnitudesInit (recipe);
+
+    // measure the aperture loss as a function of radius for PSF
+    float REF_RADIUS = psMetadataLookupF32 (&status, recipe, "PSF_REF_RADIUS");
+    psf->growth = pmGrowthCurveAlloc (PSF_FIT_PAD, 100.0, REF_RADIUS);
+
+    if (!pmGrowthCurveGenerate (readout, psf, IGNORE_GROWTH, maskVal, mark)) {
+        psError(PSPHOT_ERR_APERTURE, false, "Fitting aperture corrections");
+        psFree(psf->growth); psf->growth = NULL;
+        return false;
+    }
+
+    psVector *mask    = psVectorAllocEmpty (300, PS_TYPE_U8);
+    psVector *mag     = psVectorAllocEmpty (300, PS_TYPE_F32);
+    psVector *xPos    = psVectorAllocEmpty (300, PS_TYPE_F32);
+    psVector *yPos    = psVectorAllocEmpty (300, PS_TYPE_F32);
+    psVector *apResid = psVectorAllocEmpty (300, PS_TYPE_F32);
+    psVector *dMag    = psVectorAllocEmpty (300, PS_TYPE_F32);
+    Npsf = 0;
+
+    // select all good PM_SOURCE_TYPE_STAR entries
+    for (int i = 0; i < sources->n; i++) {
+        source = sources->data[i];
+        model = source->modelPSF;
+
+        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");
+
+        // get growth-corrected, apTrend-uncorrected magnitudes in scaled apertures
+        // will fail if below S/N threshold or model is missing
+        if (!pmSourceMagnitudes (source, psf, photMode, maskVal, mark)) {
+            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;
+        }
+
+	// aperture residual for this source
+	float dap = source->apMag - source->psfMag;
+
+        // sanity clipping : if the model is very discrepant, put your expectation in the recipe
+        if ((MAX_AP_OFFSET > 0) && (fabs(dap) > MAX_AP_OFFSET)) {
+            Nfail ++;
+	    psTrace ("psphot", 3, "fail : bad dap %f %f", dap, MAX_AP_OFFSET);
+            continue;
+        }
+
+	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.U8[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);
+        Npsf ++;
+    }
+    
+    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);
+
+    // XXX choose a better value here?
+    if (Npsf < APTREND_NSTAR_MIN) {
+        psError(PSPHOT_ERR_APERTURE, true, "Only %d valid aperture residual sources (need %d), giving up",
+                Npsf, APTREND_NSTAR_MIN);
+        return false;
+    }
+
+    // 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;
+
+    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
+    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
+	if (errorFloor < errorFloorMin) {
+	    errorFloorMin = errorFloor;
+	    entryMin = i;
+	}
+    }
+    if (entryMin == -1) {
+	psAbort ("failed on ApResid Trend");
+    }
+
+    // XXX catch error condition 
+    psphotApResidTrend (readout, psf, Npsf, entryMin, &errorScale, &errorFloor, mask, xPos, yPos, apResid, dMag);
+
+    // construct the fitted values and the residuals
+    psVector *apResidFit = pmTrend2DEvalVector (psf->ApTrend, 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") >= 5) {
+	FILE *dumpFile = fopen ("apresid.dat", "w");
+	for (int i = 0; i < xPos->n; i++) {
+	    fprintf (dumpFile, "%f %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],
+		     apResid->data.F32[i], apResidRes->data.F32[i],
+		     mask->data.U8[i]);
+	}
+	fclose (dumpFile);
+    }
+
+    // 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 sky bias",   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, "ap residual scatter", psf->growth->apLoss);
+    psMetadataAdd (recipe, PS_LIST_TAIL, "NAPMIFIT", PS_DATA_S32 | PS_META_REPLACE, "ap residual scatter", psf->nApResid);
+
+    // psLogMsg ("psphot.apresid", PS_LOG_INFO, "measure full-frame aperture residuals for %d of %d objects: %f sec\n", Nkeep, Npsf, psTimerMark ("psphot"));
+    psLogMsg ("psphot.apresid", PS_LOG_DETAIL, "aperture residual: %f +/- %f\n", psf->ApResid, psf->dApResid);
+
+    psFree (mag);
+    psFree (mask);
+    psFree (xPos);
+    psFree (yPos);
+
+    psFree (apResid);
+    psFree (apResidFit);
+    psFree (apResidRes);
+
+    psFree (dMagSys);
+    psFree (dMag);
+
+    return true;
+}
+
+/*
+  (aprMag' - fitMag) = rflux*skyBias + ApTrend(x,y)
+  (aprMag - rflux*skyBias) - fitMag = ApTrend(x,y)
+  (aprMag - rflux*skyBias) = fitMag + ApTrend(x,y)
+*/
+
+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_U8);
+
+    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.U8[j]  = mask->data.U8[N];
+	}
+	dMSubset->n = j;
+	dASubset->n = j;
+	mkSubset->n = j;
+
+	psStatsInit (statsS);
+	psStatsInit (statsM);
+
+	psVectorStats (statsS, dASubset, NULL, mkSubset, 0xff);
+	psVectorStats (statsM, dMSubset, NULL, mkSubset, 0xff);
+
+	dSo->data.F32[i] = statsS->robustStdev;
+	dMo->data.F32[i] = statsM->sampleMean;
+
+	dRo->data.F32[i] = statsS->robustStdev / statsM->sampleMean;
+    }
+    psFree (dMSubset);
+    psFree (dASubset);
+    psFree (mkSubset);
+    
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
+    psVectorStats (stats, dRo, NULL, NULL, 0);
+
+    *errorScale = stats->sampleMedian;
+    for (int i = 0; i < dSo->n; i++) {
+	*errorFloor = dSo->data.F32[i];
+	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;
+	Ny = PS_MAX (1, Nx * (readout->image->numRows / (float) readout->image->numCols));
+    } else {
+	Ny = scale;
+	Nx = PS_MAX (1, Ny * (readout->image->numCols / (float) readout->image->numRows));
+    }
+
+    if (Npsf < 3*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 test for errors here
+    pmTrend2DFit (psf->ApTrend, mask, 0xff, xPos, yPos, apResid, dMag);
+    
+    // construct the fitted values and the residuals
+    psVector *apResidFit = pmTrend2DEvalVector (psf->ApTrend, 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 (apResidFit);
+    psFree (apResidRes);
+
+    return true;
+}
+    
Index: /branches/eam_branch_20071023/psphot/src/psphotArguments.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotArguments.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotArguments.c	(revision 15358)
@@ -0,0 +1,122 @@
+# include "psphotStandAlone.h"
+
+pmConfig *psphotArguments(int argc, char **argv) {
+
+    int N;
+    bool status;
+
+    if (argc == 1) {
+        psError(PSPHOT_ERR_ARGUMENTS, true, "Too few arguments: %d", argc);
+        return NULL;
+    }
+
+    if ((N = psArgumentGet (argc, argv, "-version"))) {
+        psString version;
+        version = psphotVersionLong();    fprintf (stdout, "%s\n", version); psFree (version);
+        version = psModulesVersionLong(); fprintf (stdout, "%s\n", version); psFree (version);
+        version = psLibVersionLong();     fprintf (stdout, "%s\n", version); psFree (version);
+        exit (0);
+    }
+
+    // load config data from default locations
+    pmConfig *config = pmConfigRead(&argc, argv, PSPHOT_RECIPE);
+    if (config == NULL) {
+        psError(PSPHOT_ERR_CONFIG, false, "Can't read site configuration");
+        return NULL;
+    }
+
+    // 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);
+
+    // run the test model (requires X,Y coordinate)
+    if ((N = psArgumentGet (argc, argv, "-modeltest"))) {
+        psMetadataAddBool (options, PS_LIST_TAIL, "TEST_FIT",   0, "", true);
+        psMetadataAddF32  (options, PS_LIST_TAIL, "TEST_FIT_X", 0, "", atof(argv[N+1]));
+        psMetadataAddF32  (options, PS_LIST_TAIL, "TEST_FIT_Y", 0, "", atof(argv[N+2]));
+
+        psArgumentRemove (N, &argc, argv);
+        psArgumentRemove (N, &argc, argv);
+        psArgumentRemove (N, &argc, argv);
+
+        // specify the modeltest model
+        if ((N = psArgumentGet (argc, argv, "-model"))) {
+            psArgumentRemove (N, &argc, argv);
+            psMetadataAddStr (options, PS_LIST_TAIL, "TEST_FIT_MODEL", 0, "", argv[N]);
+            psArgumentRemove (N, &argc, argv);
+        }
+
+        // specify the test fit mode
+        if ((N = psArgumentGet (argc, argv, "-fitmode"))) {
+            psArgumentRemove (N, &argc, argv);
+            psMetadataAddStr (options, PS_LIST_TAIL, "TEST_FIT_MODE", 0, "", argv[N]);
+            psArgumentRemove (N, &argc, argv);
+        }
+        if ((N = psArgumentGet (argc, argv, "-fitset"))) {
+            psArgumentRemove (N, &argc, argv);
+            psMetadataAddStr (options, PS_LIST_TAIL, "TEST_FIT_SET", 0, "", argv[N]);
+            psArgumentRemove (N, &argc, argv);
+        }
+    }
+
+    // photcode : used in output to supplement header data (argument or recipe?)
+    if ((N = psArgumentGet (argc, argv, "-photcode"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (options, PS_LIST_TAIL, "PHOTCODE", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // break : used from recipe throughout psphotReadout
+    if ((N = psArgumentGet (argc, argv, "-break"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (options, PS_LIST_TAIL, "BREAK_POINT", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // fitmode : used from recipe throughout psphotReadout
+    if ((N = psArgumentGet (argc, argv, "-fitmode"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (options, PS_LIST_TAIL, "FITMODE", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // analysis region : overrides recipe value, used in psphotReadout/psphotEnsemblePSF
+    if ((N = psArgumentGet (argc, argv, "-region"))) {
+        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"))) {
+        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, "WEIGHT", "-weight", "-weightlist");
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "PSPHOT.PSF", "-psf", "-psflist");
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "SRC",    "-src",    "-srclist");
+
+    // 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");
+        return NULL;
+    }
+
+    if (argc != 2) {
+        psError(PSPHOT_ERR_ARGUMENTS, true, "Expected to see one more argument; saw %d", argc - 1);
+        return NULL;
+    }
+
+    // output position is fixed
+    psMetadataAddStr (config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "", argv[1]);
+
+    psTrace("psphot", 1, "Done with psphotArguments...\n");
+    return (config);
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotBasicDeblend.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotBasicDeblend.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotBasicDeblend.c	(revision 15358)
@@ -0,0 +1,129 @@
+# include "psphotInternal.h"
+
+bool psphotBasicDeblend (psArray *sources, psMetadata *recipe) {
+
+    int N;
+    bool status;
+    float threshold;
+    pmSource *source, *testSource;
+
+    int Nblend = 0;
+
+    psTimerStart ("psphot");
+
+    float FRACTION = psMetadataLookupF32 (&status, recipe, "DEBLEND_PEAK_FRACTION");
+    if (!status) FRACTION = 0.25;
+
+    float NSIGMA   = psMetadataLookupF32 (&status, recipe, "DEBLEND_SKY_NSIGMA");
+    if (!status) NSIGMA = 5.0;
+
+    // we need sources spatially-sorted to find overlaps
+    sources = psArraySort (sources, pmSourceSortByY);
+
+    // source analysis is done in peak order (brightest first)
+    // we use an index for this so the spatial sorting is kept
+    psVector *SN = psVectorAlloc (sources->n, PS_DATA_F32);
+    for (int i = 0; i < SN->n; i++) {
+        source = sources->data[i];
+        SN->data.F32[i] = source->peak->SN;
+    }
+    psVector *index = psVectorSortIndex (NULL, SN);
+    // this results in an index of increasing SN
+
+    // examine sources in decreasing SN order
+    for (int i = sources->n - 1; i >= 0; i--) {
+        N = index->data.U32[i];
+        source = sources->data[N];
+
+        if (source->mode & PM_SOURCE_MODE_BLEND) continue;
+
+        // temporary array for overlapping objects we find
+        psArray *overlap = psArrayAllocEmpty (100);
+
+        // search backwards for overlapping sources
+        for (int j = N - 1; j >= 0; j--) {
+            testSource = sources->data[j];
+            if (testSource->peak->x <  source->pixels->col0) continue;
+            if (testSource->peak->x >= source->pixels->col0 + source->pixels->numCols) continue;
+            if (testSource->peak->y <  source->pixels->row0) break;
+            if (testSource->peak->y >= source->pixels->row0 + source->pixels->numRows) {
+                fprintf (stderr, "warning: invalid condition\n");
+                continue;
+            }
+            psArrayAdd (overlap, 100, testSource);
+        }
+
+        // search forwards for overlapping sources
+        for (int j = N + 1; j < sources->n; j++) {
+            testSource = sources->data[j];
+            if (testSource->peak->x <  source->pixels->col0) continue;
+            if (testSource->peak->x >= source->pixels->col0 + source->pixels->numCols) continue;
+            if (testSource->peak->y <  source->pixels->row0) {
+                fprintf (stderr, "warning: invalid condition\n");
+                continue;
+            }
+            if (testSource->peak->y >= source->pixels->row0 + source->pixels->numRows) break;
+            psArrayAdd (overlap, 100, testSource);
+        }
+
+        if (overlap->n == 0) {
+            psFree (overlap);
+            continue;
+        }
+
+        // this source has overlapping neighbors, check for actual blends
+        // generate source contour (1/4 peak counts)
+        // set the threshold based on user inputs
+
+        // threshold is fraction of the source peak flux
+        // image is background subtracted; source->moments->Sky should always be 0.0
+        threshold = FRACTION * source->peak->SN;
+        // threshold is no less than NSIGMA
+        threshold = PS_MAX (threshold, NSIGMA);
+
+        // generate a basic contour (set of x,y coordinates at-or-below flux level)
+        psArray *contour = pmSourceContour (source->pixels, source->peak->x, source->peak->y, threshold);
+        if (contour == NULL) {
+            psFree (overlap);
+            continue;
+        }
+
+        // the source contour consists of two vectors, xv and yv.  the contour is
+        // a series of coordinate pairs, (xv[i],yv[i]) & (xv[i+1],yv[i+1]).  both
+        // coordinate pairs have the same yv[] value, with xv[i] corresponding to
+        // the left boundary and xv[i+1] corresponding to the right boundary
+
+	// a blend can only be associated with one primary source
+
+        psVector *xv = contour->data[0];
+        psVector *yv = contour->data[1];
+        for (int k = 0; k < overlap->n; k++) {
+            testSource = overlap->data[k];
+	    if (testSource->mode & PM_SOURCE_MODE_BLEND) continue;
+            if (testSource->peak->value > source->peak->value) continue;
+            for (int j = 0; j < xv->n; j+=2) {
+                if (fabs(yv->data.F32[j] - testSource->peak->y) > 0.5) continue;
+                if (xv->data.F32[j+0] > testSource->peak->x) break;
+                if (xv->data.F32[j+1] < testSource->peak->x) break;
+
+                testSource->mode |= PM_SOURCE_MODE_BLEND;
+
+                // add this to the list of source->blends
+                if (source->blends == NULL) {
+                    source->blends = psArrayAllocEmpty (16);
+                }
+                psArrayAdd (source->blends, 16, testSource);
+
+                Nblend ++;
+                j = xv->n;
+            }
+        }
+        psFree (overlap);
+        psFree (contour);
+    }
+    psLogMsg ("psphot.deblend", PS_LOG_INFO, "identified %d blended objects: %f sec\n", Nblend, psTimerMark ("psphot"));
+
+    psFree (SN);
+    psFree (index);
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotBlendFit.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotBlendFit.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotBlendFit.c	(revision 15358)
@@ -0,0 +1,98 @@
+# include "psphotInternal.h"
+
+// XXX I don't like this name
+bool psphotBlendFit (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, psMaskType maskVal) {
+
+    int Nfit = 0;
+    int Npsf = 0;
+    int Next = 0;
+    int Nfail = 0;
+    bool status;
+
+    psTimerStart ("psphot");
+
+    // source analysis is done in S/N order (brightest first)
+    sources = psArraySort (sources, pmSourceSortBySN);
+
+    // S/N limit to perform full non-linear fits
+    float FIT_SN_LIM = psMetadataLookupF32 (&status, recipe, "FULL_FIT_SN_LIM");
+
+    psphotInitLimitsPSF (recipe, readout);
+    psphotInitLimitsEXT (recipe);
+    psphotInitRadiusPSF (recipe, psf->type);
+
+    psphotFitInit ();
+
+    // 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++) {
+        // if (i%100 == 0) psphotFitSummary ();
+
+        pmSource *source = sources->data[i];
+
+        // skip non-astronomical objects (very likely defects)
+        if (source->mode &  PM_SOURCE_MODE_BLEND) 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
+        // XXX this should use peak?
+        if (source->moments == NULL) continue;
+        if (source->moments->SN < FIT_SN_LIM) continue;
+
+        // XXX this should use peak?
+        if (source->moments->x < AnalysisRegion.x0) continue;
+        if (source->moments->y < AnalysisRegion.y0) continue;
+        if (source->moments->x > AnalysisRegion.x1) continue;
+        if (source->moments->y > 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?
+        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->mode & PM_SOURCE_MODE_SUBTRACTED) {
+            pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
+        }
+        Nfit ++;
+
+        // try fitting PSFs, then try extended sources
+        // these functions subtract the resulting fitted source (XXX and update the modelFlux?)
+        if (psphotFitBlend (readout, source, psf, maskVal)) {
+            psTrace ("psphot", 5, "source at %7.1f, %7.1f is psf", source->moments->x, source->moments->y);
+            Npsf ++;
+            continue;
+        }
+        if (psphotFitBlob (readout, source, sources, psf, maskVal)) {
+            psTrace ("psphot", 5, "source at %7.1f, %7.1f is ext", source->moments->x, source->moments->y);
+            Next ++;
+            continue;
+        }
+
+        psTrace ("psphot", 5, "source at %7.1f, %7.1f failed", source->moments->x, source->moments->y);
+        Nfail ++;
+
+        // re-subtract the object, leave local sky
+        pmSourceCacheModel (source, maskVal);
+        pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
+        source->mode |= PM_SOURCE_MODE_SUBTRACTED;
+        source->mode |= PM_SOURCE_MODE_TEMPSUB;
+    }
+
+    psLogMsg ("psphot.psphotBlendFit", PS_LOG_INFO, "fit models: %f sec for %d objects (%d psf, %d ext, %d failed, %ld skipped)\n", psTimerMark ("psphot"), Nfit, Npsf, Next, Nfail, sources->n - Nfit);
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotCheckStarDistribution.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotCheckStarDistribution.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotCheckStarDistribution.c	(revision 15358)
@@ -0,0 +1,104 @@
+# include "psphotInternal.h"
+
+// examine the x,y distribution of the psfstars and extend the selection if needed
+// desired region and coverage are specified by pmPSFOptions
+// even if stars->n is at the limit, we will add more sources to fill out the area
+
+bool psphotCheckStarDistribution (psArray *stars, psArray *sources, pmPSFOptions *options) {
+
+    // count the number of PSFSTAR sources in each cell.  for polynomial representations, we
+    // use an image (Norder + 1) x (Norder + 1)
+    int Nx = options->psfTrendNx;
+    int Ny = options->psfTrendNy;
+    if (options->psfTrendMode != PM_TREND_MAP) {
+	Nx ++;
+	Ny ++;
+    }
+
+    // set up and image and an image binning structure to cover the field
+    psImage *nCell = psImageAlloc (Nx, Ny, PS_TYPE_S32);
+    psImageInit (nCell, 0);
+
+    psImageBinning *binning = psImageBinningAlloc();
+    binning->nXruff = Nx;
+    binning->nYruff = Ny;
+    binning->nXfine = options->psfFieldNx;
+    binning->nYfine = options->psfFieldNy;
+
+    psImageBinningSetScale (binning, PS_IMAGE_BINNING_CENTER);
+    psImageBinningSetSkipByOffset (binning, options->psfFieldXo, options->psfFieldYo);
+
+    // where are the PSF stars located?
+    for (int i = 0; i < stars->n; i++) {
+	
+	pmSource *source = stars->data[i];
+        if (source->peak == NULL) continue;
+        if (!(source->mode & PM_SOURCE_MODE_PSFSTAR)) continue;
+
+	int binX = psImageBinningGetRuffX (binning, source->peak->xf);
+	int binY = psImageBinningGetRuffY (binning, source->peak->yf);
+
+	assert (binX >= 0);
+	assert (binY >= 0);
+	assert (binX <  nCell->numCols);
+	assert (binY <  nCell->numRows);
+
+	nCell->data.S32[binY][binX] ++;
+    }
+	
+    // do any cells have too few PSFSTAR sources?
+    // we use 3 as a minimum (slightly arbitrary...)
+    for (int iy = 0; iy < nCell->numRows; iy++) { 
+	for (int ix = 0; ix < nCell->numCols; ix++) { 
+	    if (nCell->data.S32[iy][ix] < 3) {
+		int nNew = psphotSupplementStars (stars, sources, binning, ix, iy);
+		if (nNew) {
+		    psLogMsg ("pmObjects", 3, "added %d fainter PSF candidates to %d,%d\n", nNew, ix, iy);
+		} else {
+		    psLogMsg ("pmObjects", 3, "tried to add to %d,%d, but no more sources are available\n", ix, iy);		    
+		}
+	    }
+	}
+    }
+
+    psFree (nCell);
+    psFree (binning);
+    return true;
+}
+
+// select more possible PSF stars in the specified cell
+// sources should be sorted by SN before calling this function
+int psphotSupplementStars (psArray *stars, psArray *sources, psImageBinning *binning, int ix, int iy) {
+
+    int nNew = 0;
+
+    float Xs = psImageBinningGetFineX (binning, ix);
+    float Xe = psImageBinningGetFineX (binning, ix + 1);
+
+    float Ys = psImageBinningGetFineY (binning, iy);
+    float Ye = psImageBinningGetFineY (binning, iy + 1);
+
+    for (int i = 0; i < sources->n; i++) {
+
+	// add sources which are marked as stars, in S/N order until...
+	pmSource *source = sources->data[i];
+        if (source->peak == NULL) continue;
+        if (source->moments == NULL) continue;
+        if (source->mode & PM_SOURCE_MODE_PSFSTAR) continue;
+        if (source->type != PM_SOURCE_TYPE_STAR) continue;
+
+	float x = source->peak->xf;
+	float y = source->peak->yf;
+
+	if (x < Xs) continue;
+	if (y < Ys) continue;
+	if (x > Xe) continue;
+	if (y > Ye) continue;
+
+	source->mode |= PM_SOURCE_MODE_PSFSTAR;
+	psArrayAdd (stars, 200, source);
+
+	nNew ++;
+    }
+    return nNew;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotChoosePSF.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotChoosePSF.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotChoosePSF.c	(revision 15358)
@@ -0,0 +1,458 @@
+# include "psphotInternal.h"
+
+// try PSF models and select best option
+pmPSF *psphotChoosePSF (pmReadout *readout, psArray *sources, psMetadata *recipe, psMaskType maskVal, psMaskType mark) {
+
+    bool status;
+
+    psTimerStart ("psphot");
+
+    // examine PSF sources in S/N order (brightest first)
+    sources = psArraySort (sources, pmSourceSortBySN);
+
+    // structure to store user options defining the psf
+    pmPSFOptions *options = pmPSFOptionsAlloc ();
+
+    // load user options from the recipe. no need to check existence -- they are 
+    // array to store candidate PSF stars
+    int NSTARS = psMetadataLookupS32 (&status, recipe, "PSF_MAX_NSTARS");
+    assert (status);
+
+    float PSF_MIN_DS = psMetadataLookupF32 (&status, recipe, "PSF_MIN_DS");
+    assert (status);
+
+    // supply the measured sky variance for optional constant errors (non-poissonian)
+    float SKY_SIG = psMetadataLookupF32 (&status, recipe, "SKY_SIG");
+    assert (status);
+
+    // use poissonian errors or local-sky errors
+    options->poissonErrorsPhotLMM = psMetadataLookupBool (&status, recipe, "POISSON.ERRORS.PHOT.LMM");
+    assert (status);
+
+    // use poissonian errors or local-sky errors
+    options->poissonErrorsPhotLin = psMetadataLookupBool (&status, recipe, "POISSON.ERRORS.PHOT.LIN");
+    assert (status);
+
+    // use poissonian errors or local-sky errors
+    options->poissonErrorsParams = psMetadataLookupBool (&status, recipe, "POISSON.ERRORS.PARAMS");
+    assert (status);
+
+    // how to model the PSF variations across the field
+    options->psfTrendMode = pmTrend2DModeFromString (psMetadataLookupStr (&status, recipe, "PSF.TREND.MODE"));
+    assert (status);
+    
+    options->psfTrendNx = psMetadataLookupS32 (&status, recipe, "PSF.TREND.NX");
+    assert (status);
+
+    options->psfTrendNy = psMetadataLookupS32 (&status, recipe, "PSF.TREND.NY");
+    assert (status);
+
+    // 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);
+
+    options->stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+
+    // dimensions of the field for which the PSF is defined
+    options->psfFieldNx = readout->image->numCols;
+    options->psfFieldNy = readout->image->numRows;
+    options->psfFieldXo = readout->image->col0;
+    options->psfFieldYo = readout->image->row0;
+
+    pmSourceFitModelInit (15, 0.01, PS_SQR(SKY_SIG), options->poissonErrorsPhotLMM);
+
+    psArray *stars = psArrayAllocEmpty (sources->n);
+
+    // 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) {
+            // keep NSTARS PSF stars, unmark the rest
+            if (stars->n < NSTARS) {
+                psArrayAdd (stars, 200, source);
+            } else {
+                source->mode &= ~PM_SOURCE_MODE_PSFSTAR;
+            }
+        }
+    }
+    // check that the identified psf stars sufficiently cover the region; if not, extend the
+    // limits somewhat
+    psphotCheckStarDistribution (stars, sources, options);
+
+    psLogMsg ("psphot.pspsf", PS_LOG_DETAIL, "selected candidate %ld PSF objects\n", stars->n);
+
+    // get the list pointers for the PSF_MODEL entries
+    psArray *modelNames = NULL;
+    psMetadataItem *item = psMetadataLookup (recipe, "PSF_MODEL");
+    if (item == NULL) psAbort("missing PSF_MODEL selection");
+
+    if (item->type == PS_DATA_STRING) {
+        modelNames = psArrayAlloc(1);
+        modelNames->data[0] = psStringCopy (item->data.V);
+    } else {
+        if (item->type != PS_DATA_METADATA_MULTI) psAbort("missing PSF_MODEL selection");
+	modelNames = psListToArray (item->data.list);
+    }
+
+    // generate a psf model using the first selection
+    if (stars->n == 0) {
+	// XXX optionally give up here
+	psError(PSPHOT_ERR_PSF, false, "Failed to fit any models when choosing PSF");
+	psErrorStackPrint (stderr, "Using guess PSF model");
+	psErrorClear ();
+	
+	// unset the PSFSTAR flags (none are used):
+	for (int i = 0; i < sources->n; i++) {
+	    pmSource *source = sources->data[i];
+	    source->mode &= ~PM_SOURCE_MODE_PSFSTAR;
+	}
+
+	// XXX set sxx, etc from FWHM in recipe
+	pmPSF *psf = pmPSFBuildSimple (modelNames->data[0], 1.0, 1.0, 0.0, 1.0);
+	psf->fieldNx = readout->image->numCols;
+	psf->fieldNy = readout->image->numRows;
+	psFree (modelNames);
+
+	bool status = true;
+	status &= psphotMakeFluxScale (readout->image, recipe, psf);
+	status &= psphotPSFstats (readout, recipe, psf);
+	if (!status) {
+	    psError(PSPHOT_ERR_PSF, false, "Failed to fit any models when choosing PSF");
+	    psFree (psf);
+	    return NULL;
+	}
+
+	// XXX set DSX_MEAN, etc?
+        return psf;
+    }
+
+    // set up an array to store the results
+    psArray *models = psArrayAlloc (modelNames->n);
+
+    // try each model option listed in config
+    for (int i = 0; i < modelNames->n; i++) {
+        char *modelName = modelNames->data[i];
+        models->data[i] = pmPSFtryModel (stars, modelName, options, maskVal, mark);
+    }
+
+    // select the best of the models
+    // here we are using the clippedStdev on the metric as the indicator
+    int bestN = -1;
+    float bestM = 0.0;
+    for (int i = 0; i < models->n; i++) {
+        pmPSFtry *try = models->data[i];
+        if (try == NULL) {
+            psTrace ("psphot", 3, "PSF model %d is NULL", i);
+            continue;
+        }
+        float M = try->psf->dApResid;
+        if (bestN < 0 || M < bestM) {
+            bestM = M;
+            bestN = i;
+        }
+    }
+
+    // use the best model:
+    if (bestN < 0) {
+        psFree (models);
+	psFree (stars);
+
+	// XXX optionally give up here
+	// generate a psf model using the first selection
+	psError(PSPHOT_ERR_PSF, false, "Failed to fit any models when choosing PSF");
+	psErrorStackPrint (stderr, "Using guess PSF model");
+	psErrorClear ();
+	
+	// unset the PSFSTAR flags (none are used):
+	for (int i = 0; i < sources->n; i++) {
+	    pmSource *source = sources->data[i];
+	    source->mode &= ~PM_SOURCE_MODE_PSFSTAR;
+	}
+
+	// XXX set sxx, etc from FWHM in recipe
+	pmPSF *psf = pmPSFBuildSimple (modelNames->data[0], 1.0, 1.0, 0.0, 1.0);
+	psf->fieldNx = readout->image->numCols;
+	psf->fieldNy = readout->image->numRows;
+	psFree (modelNames);
+
+	bool status = true;
+	status &= psphotMakeFluxScale (readout->image, recipe, psf);
+	status &= psphotPSFstats (readout, recipe, psf);
+	if (!status) {
+	    psError(PSPHOT_ERR_PSF, false, "Failed to fit any models when choosing PSF");
+	    psFree (psf);
+	    return NULL;
+	}
+
+	// XXX set DSX_MEAN, etc?
+        return psf;
+    }
+
+    psFree (modelNames);
+    psFree (stars);
+
+    pmPSFtry *try = models->data[bestN];
+
+    // measure and save the median value of dparams[PM_PAR_SXX],dparams[PM_PAR_SYY]
+    // these are used by psphotEvalPSF to identify extended sources
+    // XXX is this still needed?
+    psVector *Sx = psVectorAllocEmpty (try->sources->n, PS_TYPE_F32);
+    psVector *Sy = psVectorAllocEmpty (try->sources->n, PS_TYPE_F32);
+    psVector *dSx = psVectorAllocEmpty (try->sources->n, PS_TYPE_F32);
+    psVector *dSy = psVectorAllocEmpty (try->sources->n, PS_TYPE_F32);
+    psVector *dSN = psVectorAllocEmpty (try->sources->n, PS_TYPE_F32);
+    for (int i = 0; i < try->sources->n; i++) {
+        // masked for: bad model fit, outlier in parameters
+        if (try->mask->data.U8[i] & PSFTRY_MASK_ALL)
+            continue;
+
+        pmSource *source = try->sources->data[i];
+        Sx->data.F32[Sx->n] = source->modelPSF->params->data.F32[PM_PAR_SXX];
+        Sy->data.F32[Sy->n] = source->modelPSF->params->data.F32[PM_PAR_SYY];
+        dSN->data.F32[dSN->n] = source->modelPSF->dparams->data.F32[PM_PAR_I0] / source->modelPSF->params->data.F32[PM_PAR_I0];
+        dSx->data.F32[dSx->n] = source->modelPSF->dparams->data.F32[PM_PAR_SXX];
+        dSy->data.F32[dSy->n] = source->modelPSF->dparams->data.F32[PM_PAR_SYY];
+        Sx->n ++;
+        Sy->n ++;
+        dSN->n ++;
+        dSx->n ++;
+        dSy->n ++;
+    }
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+
+    psVectorStats (stats, dSx, NULL, NULL, 0);
+    float dSxo = stats->sampleMean;
+    psLogMsg ("psphot.choosePSF", PS_LOG_INFO, "dSxo: mean: %f, median: %f, stdev: %f, npts: %ld", stats->sampleMean, stats->sampleMedian, stats->sampleStdev, dSx->n);
+
+    psVectorStats (stats, dSy, NULL, NULL, 0);
+    float dSyo = stats->sampleMean;
+    psLogMsg ("psphot.choosePSF", PS_LOG_INFO, "dSyo: mean: %f, median: %f, stdev: %f, npts: %ld", stats->sampleMean, stats->sampleMedian, stats->sampleStdev, dSy->n);
+
+    for (int i = 0; i < dSN->n; i++) {
+        dSx->data.F32[i] = (dSx->data.F32[i] - dSxo) / PS_MAX (PSF_MIN_DS, Sx->data.F32[i]*dSN->data.F32[i]);
+        dSy->data.F32[i] = (dSy->data.F32[i] - dSyo) / PS_MAX (PSF_MIN_DS, Sy->data.F32[i]*dSN->data.F32[i]);
+    }
+
+    psVectorStats (stats, dSx, NULL, NULL, 0);
+    float nSx = stats->sampleStdev;
+    psLogMsg ("psphot.choosePSF", PS_LOG_INFO, "ndSx: mean: %f, median: %f, stdev: %f, npts: %ld", stats->sampleMean, stats->sampleMedian, stats->sampleStdev, dSx->n);
+    psVectorStats (stats, dSy, NULL, NULL, 0);
+    float nSy = stats->sampleStdev;
+    psLogMsg ("psphot.choosePSF", PS_LOG_INFO, "ndSy: mean: %f, median: %f, stdev: %f, npts: %ld", stats->sampleMean, stats->sampleMedian, stats->sampleStdev, dSy->n);
+
+    psFree (Sx);
+    psFree (Sy);
+    psFree (dSx);
+    psFree (dSy);
+    psFree (dSN);
+    psFree (stats);
+
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DSX_MEAN", PS_META_REPLACE, "mean offset of dSXX", dSxo);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DSY_MEAN", PS_META_REPLACE, "mean offset of dSYY", dSyo);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DSX_STDV", PS_META_REPLACE, "stdev of mean-corrected dSXX", nSx);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DSY_STDV", PS_META_REPLACE, "stdev of mean-corrected dSYY", nSy);
+
+    // unset the PSFSTAR flag for stars not used for PSF model
+    for (int i = 0; i < try->sources->n; i++) {
+        pmSource *source = try->sources->data[i];
+        if (try->mask->data.U8[i]) {
+            source->mode &= ~PM_SOURCE_MODE_PSFSTAR;
+        }
+    }
+
+    // build a PSF residual image
+    if (!psphotMakeResiduals (try->sources, recipe, try->psf, maskVal)) {
+        psError(PSPHOT_ERR_PSF, false, "Unable to construct residual table for PSF");
+        psFree (models);
+        return NULL;
+    }
+
+    // build a PSF residual image
+    if (!psphotMakeFluxScale (readout->image, recipe, try->psf)) {
+        psError(PSPHOT_ERR_PSF, false, "Unable to construct flux scale for PSF");
+        psFree (models);
+        return NULL;
+    }
+
+    // XXX test dump of psf star data and psf-subtracted image
+    if (psTraceGetLevel("psphot.psfstars") > 5) {
+        psphotSaveImage (NULL, readout->image,  "rawstars.fits");
+
+        for (int i = 0; i < try->sources->n; i++) {
+            // masked for: bad model fit, outlier in parameters
+            if (try->mask->data.U8[i] & PSFTRY_MASK_ALL)
+                continue;
+
+            pmSource *source = try->sources->data[i];
+            float x = source->modelPSF->params->data.F32[PM_PAR_XPOS];
+            float y = source->modelPSF->params->data.F32[PM_PAR_YPOS];
+
+            // set the mask and subtract the PSF model
+            // XXX should we be using maskObj? should we be unsetting the mask?
+            // use pmModelSub because modelFlux has not been generated
+            assert (source->maskObj);
+            psImageKeepCircle (source->maskObj, x, y, options->radius, "OR", PM_MASK_MARK);
+            pmModelSub (source->pixels, source->maskObj, source->modelPSF, PM_MODEL_OP_FULL, maskVal);
+            psImageKeepCircle (source->maskObj, x, y, options->radius, "AND", PS_NOT_U8(PM_MASK_MARK));
+        }
+
+        FILE *f = fopen ("shapes.dat", "w");
+        for (int i = 0; i < try->sources->n; i++) {
+            psF32 inPar[10];  // must be psF32 to pmPSF_FitToModel
+
+            // masked for: bad model fit, outlier in parameters
+            if (try->mask->data.U8[i] & PSFTRY_MASK_ALL) continue;
+
+            pmSource *source = try->sources->data[i];
+            psF32 *outPar = source->modelEXT->params->data.F32;
+
+            psEllipseShape shape;
+
+            shape.sx  = outPar[PM_PAR_SXX] / M_SQRT2;
+            shape.sy  = outPar[PM_PAR_SYY] / M_SQRT2;
+            shape.sxy = outPar[PM_PAR_SXY];
+
+            psEllipsePol pol = pmPSF_ModelToFit (outPar);
+            inPar[PM_PAR_E0] = pol.e0;
+            inPar[PM_PAR_E1] = pol.e1;
+            inPar[PM_PAR_E2] = pol.e2;
+            pmPSF_FitToModel (inPar, 0.1);
+
+            psEllipseAxes axes1 = psEllipseShapeToAxes (shape, 20.0);
+            psEllipseAxes axes2;
+            (void)psEllipsePolToAxes(pol, 0.1, &axes2);
+            psEllipsePol pol2 = psEllipseAxesToPol (axes1);
+
+            fprintf (f, "%3d  %7.2f %7.2f  %7.4f %7.4f %7.4f  --  %7.4f %7.4f %7.4f  :  %7.4f %7.4f %7.4f  --  %7.4f %7.4f %7.4f : %7.4f %7.4f %6.1f : %7.4f %7.4f %6.1f\n",
+                     i, outPar[PM_PAR_XPOS], outPar[PM_PAR_YPOS],
+                     outPar[PM_PAR_SXX], outPar[PM_PAR_SXY], outPar[PM_PAR_SYY],
+                     pol.e0, pol.e1, pol.e2,
+                     pol2.e0, pol2.e1, pol2.e2,
+                     inPar[PM_PAR_SXX], inPar[PM_PAR_SXY], inPar[PM_PAR_SYY],
+                     axes1.major, axes1.minor, axes1.theta*PM_DEG_RAD,
+                     axes2.major, axes2.minor, axes2.theta*PM_DEG_RAD
+                     );
+        }
+        fclose (f);
+
+        psphotSaveImage (NULL, readout->image,  "psfstars.fits");
+        pmSourcesWritePSFs (try->sources, "psfstars.dat");
+        pmSourcesWriteEXTs (try->sources, "extstars.dat", false);
+	// XXX need alternative output function
+        // psMetadata *psfData = pmPSFtoMetadata (NULL, try->psf);
+        // psMetadataConfigWrite (psfData, "psfmodel.dat");
+        psLogMsg ("psphot.choosePSF", PS_LOG_INFO, "wrote out psf-subtracted image, psf data, exiting\n");
+        exit (0);
+    }
+
+    // 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)) {
+        psError(PSPHOT_ERR_PSF, false, "cannot measure PSF shape terms");
+        psFree(psf);
+        return NULL;
+    }
+
+    char *modelName = pmModelClassGetName (psf->type);
+    psLogMsg ("psphot.pspsf", PS_LOG_INFO, "select psf model: %f sec\n", psTimerMark ("psphot"));
+    psLogMsg ("psphot.pspsf", PS_LOG_INFO, "psf model %s, ApResid: %f +/- %f\n", modelName, psf->ApResid, psf->dApResid);
+
+    psFree (options);
+    return (psf);
+}
+
+// measure average parameters of the PSF model
+bool psphotPSFstats (pmReadout *readout, psMetadata *recipe, pmPSF *psf) {
+
+    psEllipseShape shape;
+    psEllipseAxes axes;
+
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(recipe, false);
+    PS_ASSERT_PTR_NON_NULL(psf, false);
+
+    psImage *image = readout->image;
+    PS_ASSERT_PTR_NON_NULL(image, false);
+
+    // construct a normalized PSF model at this coordinate (Io = 1.0)
+    pmModel *modelPSF = pmModelFromPSFforXY (psf, 0.5*image->numCols, 0.5*image->numRows, 1.0);
+    if (modelPSF == NULL) {
+        psError(PSPHOT_ERR_PSF, false, "Failed to estimate PSF model at image centre");
+        return false;
+    }
+
+    // get the model full-width at half-max
+    double FWHM_X = 2*modelPSF->modelRadius (modelPSF->params, 0.5);
+
+    // XXX make sure this is consistent with the re-definition of PM_PAR_SXX
+    shape.sx  = modelPSF->params->data.F32[PM_PAR_SXX];
+    shape.sy  = modelPSF->params->data.F32[PM_PAR_SYY];
+    shape.sxy = modelPSF->params->data.F32[PM_PAR_SXY];
+    axes = psEllipseShapeToAxes (shape, 20.0);
+
+    double FWHM_Y = FWHM_X * (axes.minor / axes.major);
+
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FWHM_X",   PS_META_REPLACE, "PSF FWHM Major axis", FWHM_X);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FWHM_Y",   PS_META_REPLACE, "PSF FWHM Minor axis", FWHM_Y);
+    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);
+    psFree (modelPSF);
+
+    return true;
+}
+
+// determine approximate PSF shape parameters based on the moments
+bool psphotMomentsStats (pmReadout *readout, psMetadata *recipe, psArray *sources) {
+
+    // without the PSF model, we can only rely on the source->moments
+    // as a measure of the FWHM values
+
+    int FWHM_N = 0;
+    double FWHM_X = 0.0;
+    double FWHM_Y = 0.0;
+    double FWHM_T = 0.0;
+
+    psEllipseMoments moments;
+    psEllipseAxes axes;
+
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(recipe, false);
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+        if (!source) continue;
+        if (!(source->mode & PM_SOURCE_MODE_PSFSTAR)) continue;
+
+        // moments->Sx,Sy are roughly sigma_x,y
+        moments.x2 = PS_SQR(source->moments->Sx);
+        moments.y2 = PS_SQR(source->moments->Sy);
+        moments.xy = source->moments->Sxy;
+
+        // limit axis ratio < 20.0
+        axes = psEllipseMomentsToAxes (moments, 20.0);
+
+        FWHM_X += axes.major * 2.35;
+        FWHM_Y += axes.minor * 2.35;
+        FWHM_T += axes.theta;
+        FWHM_N ++;
+    }
+
+    FWHM_X /= FWHM_N;
+    FWHM_Y /= FWHM_N;
+    FWHM_T /= FWHM_N;
+
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FWHM_X",   PS_META_REPLACE, "PSF FWHM Major axis (from moments)", FWHM_X);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FWHM_Y",   PS_META_REPLACE, "PSF FWHM Minor axis (from moments)", FWHM_Y);
+    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);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotCleanup.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotCleanup.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotCleanup.c	(revision 15358)
@@ -0,0 +1,36 @@
+# include "psphotStandAlone.h"
+
+void psphotCleanup (pmConfig *config) {
+
+    psFree (config);
+
+    psTimerStop ();
+    psMemCheckCorruption (stderr, true);
+    pmModelClassCleanup ();
+    psTimeFinalize ();
+    pmConceptsDone ();
+    pmConfigDone ();
+    // fprintf (stderr, "found %d leaks at %s\n", psMemCheckLeaks (0, NULL, NULL, false), "psphot");
+    fprintf (stderr, "found %d leaks at %s\n", psMemCheckLeaks (0, NULL, stdout, false), "psphot");
+    return;
+}
+
+psExit psphotGetExitStatus () {
+
+    psErrorCode err = psErrorCodeLast ();
+    switch (err) {
+      case PS_ERR_NONE:
+	return PS_EXIT_SUCCESS;
+      case PSPHOT_ERR_SYS:
+        return PS_EXIT_SYS_ERROR;
+      case PSPHOT_ERR_CONFIG:
+        return PS_EXIT_CONFIG_ERROR;
+      case PSPHOT_ERR_PROG:
+        return PS_EXIT_PROG_ERROR;
+      case PSPHOT_ERR_DATA:
+        return PS_EXIT_DATA_ERROR;
+      default:
+        return PS_EXIT_UNKNOWN_ERROR;
+    }
+    return PS_EXIT_UNKNOWN_ERROR;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotDeblendSatstars.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotDeblendSatstars.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotDeblendSatstars.c	(revision 15358)
@@ -0,0 +1,159 @@
+# include "psphotInternal.h"
+
+bool psphotDeblendSatstars (psArray *sources, psMetadata *recipe) {
+
+    int N;
+    pmSource *source;
+
+    psTimerStart ("psphot");
+
+    int Nblend = 0;
+    float SAT_TEST_LEVEL = 50000;
+    float SAT_MIN_RADIUS = 5.0;
+
+    // we need sources spatially-sorted to find overlaps
+    sources = psArraySort (sources, pmSourceSortByY);
+
+    // source analysis is done in peak order (brightest first)
+    // we use an index for this so the spatial sorting is kept
+    psVector *SN = psVectorAlloc (sources->n, PS_DATA_F32);
+    for (int i = 0; i < SN->n; i++) {
+        source = sources->data[i];
+        SN->data.F32[i] = source->peak->SN;
+    }
+    psVector *index = psVectorSortIndex (NULL, SN);
+    // this results in an index of increasing SN
+
+    // examine sources in decreasing SN order
+    for (int i = sources->n - 1; i >= 0; i--) {
+        N = index->data.U32[i];
+        source = sources->data[N];
+
+        // XXX filter? if (source->mode & PM_SOURCE_MODE_SATSTAR) continue;
+        if (source->mode & PM_SOURCE_MODE_BLEND) continue;
+        if (source->peak->flux < SAT_TEST_LEVEL) continue;
+
+	// save these for reference below
+	int xPeak = source->peak->x;
+	int yPeak = source->peak->y;
+
+        // generate a basic contour (set of x,y coordinates at-or-below flux level)
+        psArray *contour = pmSourceContour (source->pixels, xPeak, yPeak, SAT_TEST_LEVEL);
+        if (contour == NULL) continue;
+
+	// contour consists of a set of X,Y coords giving the boundary
+	psVector *xVec = contour->data[0];
+	psVector *yVec = contour->data[1];
+
+	// XXX should we filter based on the number of pixels in the contour?
+
+	// find the center of the contour (let's just use mid[x,y])
+	int xMin = xVec->data.F32[0];
+	int xMax = xVec->data.F32[0];
+	int yMin = yVec->data.F32[0];
+	int yMax = yVec->data.F32[0];
+	for (int j = 0; j < xVec->n; j++) {
+	    xMin = PS_MIN (xMin, xVec->data.F32[j]);
+	    xMax = PS_MAX (xMax, xVec->data.F32[j]);
+	    yMin = PS_MIN (yMin, yVec->data.F32[j]);
+	    yMax = PS_MAX (yMax, yVec->data.F32[j]);
+	}	
+	int xCenter = 0.5*(xMin + xMax);
+	int yCenter = 0.5*(yMin + yMax);
+	psFree (contour);
+
+	// reset the peak for this source to the value of the center pixel
+	source->peak->x = xCenter;
+	source->peak->y = yCenter;
+	source->peak->xf = xCenter;
+	source->peak->yf = yCenter;
+	
+	// temporary array for overlapping objects we find
+        psArray *overlap = psArrayAllocEmpty (100);
+
+        // search backwards for overlapping sources
+        for (int j = N - 1; j >= 0; j--) {
+            pmSource *testSource = sources->data[j];
+            if (testSource->peak->x <  source->pixels->col0) continue;
+            if (testSource->peak->x >= source->pixels->col0 + source->pixels->numCols) continue;
+            if (testSource->peak->y <  source->pixels->row0) break;
+            if (testSource->peak->y >= source->pixels->row0 + source->pixels->numRows) {
+                fprintf (stderr, "warning: invalid condition\n");
+                continue;
+            }
+            psArrayAdd (overlap, 100, testSource);
+        }
+
+        // search forwards for overlapping sources
+        for (int j = N + 1; j < sources->n; j++) {
+            pmSource *testSource = sources->data[j];
+            if (testSource->peak->x <  source->pixels->col0) continue;
+            if (testSource->peak->x >= source->pixels->col0 + source->pixels->numCols) continue;
+            if (testSource->peak->y <  source->pixels->row0) {
+                fprintf (stderr, "warning: invalid condition\n");
+                continue;
+            }
+            if (testSource->peak->y >= source->pixels->row0 + source->pixels->numRows) break;
+            psArrayAdd (overlap, 100, testSource);
+        }
+
+        if (overlap->n == 0) {
+            psFree (overlap);
+            continue;
+        }
+
+	// now find the contour which is at 0.5*SAT_TEST_LEVEL (xPeak, yPeak is valid high pixel)
+        contour = pmSourceContour (source->pixels, xPeak, yPeak, 0.5*SAT_TEST_LEVEL);
+        if (contour == NULL) {
+	    psFree (overlap);
+	    continue; 
+	}
+	
+	// any peaks within this contour should be dropped (mark as blend, drop after loop is done)
+	// also drop any peaks which are too close to this peal
+        psVector *xv = contour->data[0];
+        psVector *yv = contour->data[1];
+        for (int k = 0; k < overlap->n; k++) {
+            pmSource *testSource = overlap->data[k];
+	    float radius = hypot((testSource->peak->x - xCenter), (testSource->peak->y - yCenter));
+	    if (radius < SAT_MIN_RADIUS) {
+                testSource->mode |= PM_SOURCE_MODE_BLEND;
+                Nblend ++;
+		continue;
+	    }
+            for (int j = 0; j < xv->n; j+=2) {
+                if (fabs(yv->data.F32[j] - testSource->peak->y) > 0.5) continue;
+                if (xv->data.F32[j+0] > testSource->peak->x) break;
+                if (xv->data.F32[j+1] < testSource->peak->x) break;
+                testSource->mode |= PM_SOURCE_MODE_BLEND;
+                Nblend ++;
+                j = xv->n; // skip rest of contour
+            }
+        }
+        psFree (overlap);
+        psFree (contour);
+    }
+
+    // drop the sources marked as BLEND
+    for (int i = 0; i < sources->n;) {
+	pmSource *source = sources->data[i];
+
+        if (!(source->mode & PM_SOURCE_MODE_BLEND)) {
+	    i++;
+	    continue;
+	}
+
+	// shuffle the remaining sources forward
+	for (int j = i; j < sources->n - 1; j++) {
+	    sources->data[j] = sources->data[j+1];
+	}
+	psFree (source);
+	sources->n --;
+    }
+
+    psFree (SN);
+    psFree (index);
+
+    psLogMsg ("psphot", PS_LOG_INFO, "found %d satstar blend peaks, leaving %ld sources: %f sec\n", Nblend, sources->n, psTimerMark ("psphot"));
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotDefineFiles.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotDefineFiles.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotDefineFiles.c	(revision 15358)
@@ -0,0 +1,134 @@
+# include "psphotInternal.h"
+
+// XXX we need to be able to distinguish several cases:
+// 1) the particular output data was not requested
+// 2) the particular output data was requested but was not generated (skipped)
+// 3) the particular output data was requested but was not generated (for valid failure)
+// 4) the particular output data was requested but was not generated (surprising failure)
+
+// define the needed / desired I/O files
+bool psphotDefineFiles (pmConfig *config, pmFPAfile *input) {
+
+    bool status = false;
+
+    // select recipe options supplied on command line
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+
+    // the output sources are carried on the input->fpa structures
+    pmFPAfile *outsources = pmFPAfileDefineOutputFromFile (config, input, "PSPHOT.OUTPUT");
+    if (!outsources) {
+        psError(PSPHOT_ERR_CONFIG, false, "Cannot find a rule for PSPHOT.OUTPUT");
+        return false;
+    }
+    outsources->save = true;
+
+    // if we are choosing not to save the output detections, mark this file inactive so it will
+    // not be written, but will keep the detections in memory for other functions (eg, astrometry)
+    if (!psMetadataLookupBool (NULL, recipe, "SAVE.OUTPUT")) {
+        pmFPAfileActivate (config->files, false, "PSPHOT.OUTPUT");
+        outsources->save = false;
+    }
+
+    // optionally save the residual image
+    if (psMetadataLookupBool(NULL, recipe, "SAVE.RESID")) {
+        pmFPAfile *output = pmFPAfileDefineOutputFromFile (config, input, "PSPHOT.RESID");
+        if (!output) {
+            psError(PSPHOT_ERR_CONFIG, false, "Cannot find a rule for PSPHOT.RESID");
+            return false;
+        }
+        output->save = true;
+    }
+    // optionally save the background model (small FITS image)
+    if (psMetadataLookupBool(NULL, recipe, "SAVE.BACKMDL")) {
+        int DX = psMetadataLookupS32 (&status, recipe, "BACKGROUND.XBIN");
+        int DY = psMetadataLookupS32 (&status, recipe, "BACKGROUND.YBIN");
+        pmFPAfile *output = pmFPAfileDefineFromFile (config, input, DX, DY, "PSPHOT.BACKMDL");
+        if (!output) {
+            psError(PSPHOT_ERR_CONFIG, false, "Cannot find a rule for PSPHOT.BACKMDL");
+            return false;
+        }
+        output->save = true;
+    }
+    // optionally save the background model's standard deviation (small FITS image)
+    if (psMetadataLookupBool(NULL, recipe, "SAVE.BACKMDL.STDEV")) {
+        int DX = psMetadataLookupS32 (&status, recipe, "BACKGROUND.XBIN");
+        int DY = psMetadataLookupS32 (&status, recipe, "BACKGROUND.YBIN");
+        pmFPAfile *output = pmFPAfileDefineFromFile (config, input, DX, DY, "PSPHOT.BACKMDL.STDEV");
+        if (!output) {
+            psError(PSPHOT_ERR_CONFIG, false, "Cannot find a rule for PSPHOT.BACKMDL.STDEV");
+            return false;
+        }
+        output->save = true;
+    }
+    // optionally save the full background image
+    if (psMetadataLookupBool(NULL, recipe, "SAVE.BACKGND")) {
+        pmFPAfile *output = pmFPAfileDefineFromFile (config, input,  1,  1, "PSPHOT.BACKGND");
+        if (!output) {
+            psError(PSPHOT_ERR_CONFIG, false, "Cannot find a rule for PSPHOT.BACKGND");
+            return false;
+        }
+        output->save = true;
+    }
+    // optionally save the background-subtracted image
+    if (psMetadataLookupBool(NULL, recipe, "SAVE.BACKSUB")) {
+        pmFPAfile *output = pmFPAfileDefineFromFile (config, input,  1,  1, "PSPHOT.BACKSUB");
+        if (!output) {
+            psError(PSPHOT_ERR_CONFIG, false, "Cannot find a rule for PSPHOT.");
+            return false;
+        }
+        output->save = true;
+    }
+    // optionally save the PSF Model
+    if (psMetadataLookupBool(NULL, recipe, "SAVE.PSF")) {
+        pmFPAfile *output = pmFPAfileDefineOutputFromFile (config, input, "PSPHOT.PSF.SAVE");
+        if (!output) {
+            psError(PSPHOT_ERR_CONFIG, false, "Cannot find a rule for PSPHOT.PSF.SAVE");
+            return false;
+        }
+        output->save = true;
+    }
+
+    // optionally save output plots
+    // allow specific plots only
+    if (psMetadataLookupBool(NULL, recipe, "SAVE.PLOTS")) {
+        pmFPAfile *output = NULL;
+        output = pmFPAfileDefineOutputFromFile (config, input, "SOURCE.PLOT.MOMENTS");
+        if (!output) {
+            psError(PSPHOT_ERR_CONFIG, false, "Cannot find a rule for SOURCE.PLOT.MOMENTS");
+            return false;
+        }
+        output->save = true;
+        output = pmFPAfileDefineOutputFromFile (config, input, "SOURCE.PLOT.PSFMODEL");
+        if (!output) {
+            psError(PSPHOT_ERR_CONFIG, false, "Cannot find a rule for SOURCE.PLOT.PSFMODEL");
+            return false;
+        }
+        output->save = true;
+        output = pmFPAfileDefineOutputFromFile (config, input, "SOURCE.PLOT.APRESID");
+        if (!output) {
+            psError(PSPHOT_ERR_CONFIG, false, "Cannot find a rule for SOURCE.PLOT.APRESID");
+            return false;
+        }
+        output->save = true;
+    }
+
+    if (psMetadataLookupPtr(NULL, config->arguments, "SRC")) {
+        if (!pmFPAfileDefineFromArgs (&status, config, "PSPHOT.INPUT.CMF", "SRC")) {
+            psError(PSPHOT_ERR_CONFIG, false, "Failed to find/build PSPHOT.INPUT.CMF");
+            return status;
+        }
+    }
+
+    if (psMetadataLookupPtr(NULL, config->arguments, "PSPHOT.PSF")) {
+        pmFPAfileBindFromArgs(&status, input, config, "PSPHOT.PSF.LOAD", "PSPHOT.PSF");
+        if (!status) {
+            psError(PSPHOT_ERR_CONFIG, false, "Failed to find/build PSPHOT.PSF.LOAD");
+            return status;
+        }
+    }
+
+    // XXX add in example PSF image thumbnails
+    // pmFPAfileConstruct (config->files, format, config->camera, "PSPHOT.PSF_SAMPLE");
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotDiagnosticPlots.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotDiagnosticPlots.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotDiagnosticPlots.c	(revision 15358)
@@ -0,0 +1,186 @@
+# include "psphotInternal.h"
+
+# if (HAVE_KAPA)
+
+static int kapa_fd = -1;
+
+int psphotKapaOpen ()
+{
+    char kapa[64];
+
+    strcpy (kapa, "kapa");
+
+    if (kapa_fd == -1) {
+        kapa_fd = KapaOpenNamedSocket (kapa, "psphot");
+    }
+    return kapa_fd;
+}
+
+bool psphotKapaClose ()
+{
+
+    if (kapa_fd == -1)
+        return true;
+    KapaClose (kapa_fd);
+    kapa_fd = -1;
+    return true;
+}
+
+bool psphotImageBackgroundCellHistogram (psVector *values, float mean, float sigma, int ix, int iy)
+{
+
+    KapaSection section;
+    Graphdata graphdata;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    psStats *stats = psStatsAlloc (PS_STAT_MAX | PS_STAT_MIN);
+    psVectorStats (stats, values, NULL, NULL, 0);
+
+    psHistogram *histogram = psHistogramAlloc (stats->min, stats->max, 1000);
+    psVectorHistogram (histogram, values, NULL, NULL, 0);
+
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa);
+
+    // plot 1 is the full range
+    section.x  = 0.0;
+    section.y  = 0.0;
+    section.dx = 1.0;
+    section.dy = 0.5;
+    section.name = strcreate ("bottom");
+    KapaSetSection (kapa, &section);
+    free (section.name);
+
+    // set limits based on data values
+    graphdata.xmin = stats->min;
+    graphdata.xmax = stats->max;
+
+    psStatsInit (stats);
+    stats->options = PS_STAT_MAX | PS_STAT_MIN;
+    psVectorStats (stats, histogram->nums, NULL, NULL, 0);
+
+    // scale the plot to hold the histogram
+    graphdata.ymin = stats->min - 0.05*(stats->max - stats->min);
+    graphdata.ymax = stats->max + 0.05*(stats->max - stats->min);
+
+    KapaSetLimits (kapa, &graphdata);
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, &graphdata);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 1;
+    KapaPrepPlot (kapa, histogram->nums->n, &graphdata);
+    KapaPlotVector (kapa, histogram->nums->n, histogram->bounds->data.F32, "x");
+    KapaPlotVector (kapa, histogram->nums->n, histogram->nums->data.F32, "y");
+    psFree (histogram);
+
+    // plot 2 is the +/- 10 sigma
+    section.x  = 0.0;
+    section.y  = 0.5;
+    section.dx = 1.0;
+    section.dy = 0.5;
+    section.name = strcreate ("top");
+    KapaSetSection (kapa, &section);
+    free (section.name);
+
+    // +/- 10 sigma
+    graphdata.xmin = mean - 10.0*sigma;
+    graphdata.xmax = mean + 10.0*sigma;
+
+    histogram = psHistogramAlloc (graphdata.xmin, graphdata.xmax, 100);
+    psVectorHistogram (histogram, values, NULL, NULL, 0);
+
+    psStatsInit (stats);
+    stats->options = PS_STAT_MAX | PS_STAT_MIN;
+    psVectorStats (stats, histogram->nums, NULL, NULL, 0);
+
+    // scale the plot to hold the histogram
+    graphdata.ymin = stats->min - 0.05*(stats->max - stats->min);
+    graphdata.ymax = stats->max + 0.05*(stats->max - stats->min);
+
+    KapaSetLimits (kapa, &graphdata);
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, &graphdata);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 1;
+    KapaPrepPlot (kapa, histogram->nums->n, &graphdata);
+    KapaPlotVector (kapa, histogram->nums->n, histogram->bounds->data.F32, "x");
+    KapaPlotVector (kapa, histogram->nums->n, histogram->nums->data.F32, "y");
+
+    char line[128];
+    sprintf (line, "sky: %f +/- %f, cell %d,%d", mean, sigma, ix, iy);
+    KapaSendLabel (kapa, line, KAPA_LABEL_XP);
+
+    // pause until user types 'return'
+    fprintf (stdout, "press return");
+    fgets (line, 64, stdin);
+
+    psFree (stats);
+    psFree (histogram);
+
+    return true;
+}
+
+bool psphotDiagnosticPlots (pmConfig *config, char *name, ...) {
+
+    va_list argPtr;
+    bool status;
+
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    assert (recipe);
+
+    psMetadata *plots = psMetadataLookupPtr (&status, recipe, "DIAGNOSTIC.PLOTS");
+    assert (plots);
+
+    // do we want the requested plot?
+    if (!psMetadataLookupBool (&status, plots, name)) return false;
+    
+    // Get the variable list parameters to pass to allocation function
+    va_start(argPtr, name);
+
+    if (!strcmp(name, "IMAGE.BACKGROUND.CELL.HISTOGRAM")) {
+
+	int ix = va_arg(argPtr, psS32);
+	int iy = va_arg(argPtr, psS32);
+	float mean  = va_arg(argPtr, double);
+	float sigma = va_arg(argPtr, double);
+	psVector *values = va_arg(argPtr, psPtr);
+	
+	int xPlot = psMetadataLookupS32 (&status, plots, "IMAGE.BACKGROUND.CELL.HISTOGRAM.X");
+	assert (status);
+	int yPlot = psMetadataLookupS32 (&status, plots, "IMAGE.BACKGROUND.CELL.HISTOGRAM.Y");
+	assert (status);
+
+	bool gotX = (xPlot < 0) || (xPlot == ix);
+	bool gotY = (yPlot < 0) || (yPlot == iy);
+
+	if (gotX && gotY) {
+	    psphotImageBackgroundCellHistogram (values, mean, sigma, ix, iy);
+	    goto done;
+	}
+    }
+
+    // Clean up stack after variable arguement has been used
+done:
+    va_end(argPtr);
+    return true;
+}
+
+# else
+
+int psphotKapaOpen () { return -1; }
+bool psphotKapaClose () { return true; }
+bool psphotImageBackgroundCellHistogram (psVector *values) { return true; }
+bool psphotDiagnosticPlots (pmConfig *config, char *name, ...) { return true; }
+
+# endif
Index: /branches/eam_branch_20071023/psphot/src/psphotEnsemblePSF.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotEnsemblePSF.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotEnsemblePSF.c	(revision 15358)
@@ -0,0 +1,331 @@
+# include "psphotInternal.h"
+
+// 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
+
+// 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 SetBorderT (psSparseBorder *border, pmReadout *readout, psArray *refSources, psArray *fitSources, psVector *index, bool constant_weights, int SKY_FIT_ORDER);
+
+bool psphotEnsemblePSF (pmReadout *readout, psArray *refSources, psMetadata *recipe, pmPSF *psf, bool final) {
+
+    bool isPSF;
+    bool status;
+    float x;
+    float y;
+    float f;
+    // float r;
+
+    psTimerStart ("psphot");
+
+    // source analysis is done in spatial order
+    refSources = psArraySort (refSources, pmSourceSortByY);
+
+    // storage arrays for fitSources and sequence index
+    psArray *fitSources = psArrayAllocEmpty (refSources->n);
+    psVector *index = psVectorAllocEmpty (refSources->n, PS_TYPE_U32);
+
+    // option to limit analysis to a specific region
+    char *region = psMetadataLookupStr (&status, recipe, "ANALYSIS_REGION");
+    psRegion AnalysisRegion = psRegionFromString (region);
+    AnalysisRegion = psRegionForImage (readout->image, AnalysisRegion);
+    if (psRegionIsNaN (AnalysisRegion)) psAbort("analysis region mis-defined");
+
+    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");
+    }
+    int SKY_FIT_ORDER = psMetadataLookupS32(&status, recipe, "SKY_FIT_ORDER");
+    if (!status) {
+	SKY_FIT_ORDER = 0;
+    }
+    bool SKY_FIT_LINEAR = psMetadataLookupBool(&status, recipe, "SKY_FIT_LINEAR");
+    if (!status) {
+	SKY_FIT_LINEAR = false;
+    }
+
+    // construct source table with stand-alone fitSources
+    for (int i = 0; i < refSources->n; i++) {
+        pmSource *refSource = refSources->data[i];
+
+        // skip non-astronomical objects (very likely defects)
+        if (refSource->type == PM_SOURCE_TYPE_DEFECT) continue;
+        if (refSource->type == PM_SOURCE_TYPE_SATURATED) continue;
+        if (final) {
+            if (refSource->mode &  PM_SOURCE_MODE_SUBTRACTED) continue;
+        } else {
+            if (refSource->mode &  PM_SOURCE_MODE_BLEND) continue;
+        }
+
+	// which model to use?
+	pmModel *model = pmSourceGetModel (&isPSF, refSource);
+	if (model == NULL) continue;  // model must be defined
+
+        // save the original coords
+        x = model->params->data.F32[PM_PAR_XPOS];
+        y = model->params->data.F32[PM_PAR_YPOS];
+
+	// is the source in the region of interest?
+        if (x < AnalysisRegion.x0) continue;
+        if (y < AnalysisRegion.y0) continue;
+        if (x > AnalysisRegion.x1) continue;
+        if (y > AnalysisRegion.y1) continue;
+
+        pmSource *fitSource = pmSourceAlloc ();
+
+        // make temporary copies of the image pixels and mask
+	// we need to have a copy which will not be modified by changes to its neighbor
+        fitSource->mask   = psImageCopy (NULL, refSource->mask,   PS_TYPE_U8);
+        fitSource->pixels = psImageCopy (NULL, refSource->pixels, PS_TYPE_F32);
+        fitSource->weight = psImageCopy (NULL, refSource->weight, PS_TYPE_F32);
+
+        // set model to unit peak, zero sky (we assume sky is subtracted)
+	// XXX do this with the actual model or use keep the original guess?
+        model->params->data.F32[PM_PAR_SKY] = 0.0;
+        model->params->data.F32[PM_PAR_I0] = 1.0;
+
+        // fill in the model pixel values
+	// XXX this function should just work with the (normalized) modelFlux pixels
+	// XXX we should not have to create a complete copy
+	// XXX review the use of the object mask: can we use a copied mask?
+        psImageInit (fitSource->pixels, 0.0);
+        psImageKeepCircle (fitSource->mask, x, y, model->radiusFit, "OR", PM_MASK_MARK);
+        pmModelAdd (fitSource->pixels, fitSource->mask, model, PM_MODEL_OP_FULL);
+
+        // save source in list
+	if (isPSF) {
+	  fitSource->modelPSF = psMemIncrRefCounter (model);
+	} else {
+	  fitSource->modelEXT = psMemIncrRefCounter (model);
+	}
+        index->data.U32[fitSources->n] = i;
+        psArrayAdd (fitSources, 100, fitSource);
+        psFree (fitSource);
+    }
+    psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "built fitSources: %f (%ld objects)\n", psTimerMark ("psphot"), refSources->n);
+
+    // vectors to store stats for each object
+    // psVector *weight = psVectorAlloc (fitSources->n, PS_TYPE_F32);
+    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
+    psSparse *sparse = psSparseAlloc (fitSources->n, 100);
+    int nBorder = (SKY_FIT_ORDER == 0) ? 1 : 3;
+    psSparseBorder *border = psSparseBorderAlloc (sparse, nBorder);
+
+    // fill out the sparse matrix elements and border elements (B)
+    // Ri is the current refSource of interest (sci pixels)
+    // Fi is the current fitSource of interest (fit pixels)
+    // note that the sci pixels are real image pixels while the fit pixels are unique for each
+    // fitted source -- they hold the model flux
+
+    for (int i = 0; i < fitSources->n; i++) {
+        int N = index->data.U32[i];
+        pmSource *Ri = refSources->data[N];
+        pmSource *Fi = fitSources->data[i];
+
+        // diagonal elements of the sparse matrix (auto-cross-product)
+        f = pmSourceCrossProduct (Fi, Fi, CONSTANT_PHOTOMETRIC_WEIGHTS);
+        psSparseMatrixElement (sparse, i, i, f);
+
+        // XXX being used? weight->data.F32[i] = r;
+
+	// the formal error depends on the weighting scheme
+	if (CONSTANT_PHOTOMETRIC_WEIGHTS) {
+	    float var = pmSourceCrossProduct (Fi, Fi, false);
+	    errors->data.F32[i] = 1.0 / sqrt(var);
+	} else {
+	    errors->data.F32[i] = 1.0 / sqrt(f);
+	}
+
+
+        // find the image x model value
+        f = pmSourceCrossProduct (Ri, Fi, CONSTANT_PHOTOMETRIC_WEIGHTS);
+        psSparseVectorElement (sparse, i, f);
+
+	// add the per-source weights (border region)
+	switch (SKY_FIT_ORDER) {
+	  case 1:
+	    f = pmSourceWeight (Fi, 1, CONSTANT_PHOTOMETRIC_WEIGHTS);
+	    psSparseBorderElementB (border, i, 1, f);
+	    f = pmSourceWeight (Fi, 2, CONSTANT_PHOTOMETRIC_WEIGHTS);
+	    psSparseBorderElementB (border, i, 2, f);
+
+	  case 0:
+	    f = pmSourceWeight (Fi, 0, CONSTANT_PHOTOMETRIC_WEIGHTS);
+	    psSparseBorderElementB (border, i, 0, f);
+	    break;
+
+	  default:
+	    psAbort("Invalid SKY_FIT_ORDER %d\n", SKY_FIT_ORDER);
+	    break;
+	}
+
+        // loop over all other stars following this one
+        for (int j = i + 1; j < fitSources->n; j++) {
+            pmSource *Fj = fitSources->data[j];
+
+            // skip over disjoint source images, break after last possible overlap
+            if (Fi->pixels->row0 + Fi->pixels->numRows < Fj->pixels->row0) break;
+            if (Fj->pixels->row0 + Fj->pixels->numRows < Fi->pixels->row0) continue;
+            if (Fi->pixels->col0 + Fi->pixels->numCols < Fj->pixels->col0) continue;
+            if (Fj->pixels->col0 + Fj->pixels->numCols < Fi->pixels->col0) continue;
+
+            // got an overlap; calculate cross-product and add to output array
+            f = pmSourceCrossProduct (Fi, Fj, CONSTANT_PHOTOMETRIC_WEIGHTS);
+            psSparseMatrixElement (sparse, j, i, f);
+        }
+    }
+
+    psSparseResort (sparse);
+    psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "built matrix: %f (%d elements)\n", psTimerMark ("psphot"), sparse->Nelem);
+
+    // set the sky, sky_x, sky_y components of border matrix
+    SetBorderT (border, readout, refSources, fitSources, index, CONSTANT_PHOTOMETRIC_WEIGHTS, SKY_FIT_ORDER);
+
+    psSparseConstraint constraint;
+    constraint.paramMin   = 0.0;
+    constraint.paramMax   = 1e8;
+    constraint.paramDelta = 1e8;
+
+    // solve for normalization terms (need include local sky?)
+    psVector *norm = NULL;
+    psVector *skyfit = NULL;
+    if (SKY_FIT_LINEAR) {
+	psSparseBorderSolve (&norm, &skyfit, constraint, border, 5);
+	fprintf (stderr, "skyfit: %f\n", skyfit->data.F32[0]);
+    } else {
+	norm = psSparseSolve (NULL, constraint, sparse, 5);
+	skyfit = NULL;
+    }
+
+    // adjust fitSources, set refSources and subtract
+    for (int i = 0; i < fitSources->n; i++) {
+        int N = index->data.U32[i];
+        pmSource *Ri = refSources->data[N];
+	pmModel *model = pmSourceGetModel (NULL, Ri);
+
+        // assign linearly-fitted normalization
+        if (isnan(norm->data.F32[i])) {
+            psAbort("ensemble source is nan");
+        }
+        model->params->data.F32[PM_PAR_I0] = norm->data.F32[i];
+	// XXX note: is the value of 'errors' modified by the sky fit?
+        model->dparams->data.F32[PM_PAR_I0] = errors->data.F32[i];
+
+        // subtract object
+        pmModelSub (Ri->pixels, Ri->mask, model, PM_MODEL_OP_FULL);
+        Ri->mode |= PM_SOURCE_MODE_SUBTRACTED;
+        if (!final) Ri->mode |= PM_SOURCE_MODE_TEMPSUB;
+	// XXX not sure about the use of TEMPSUB
+    }
+
+    // measure chisq for each source
+    for (int i = 0; final && (i < fitSources->n); i++) {
+        int N = index->data.U32[i];
+        pmSource *Ri = refSources->data[N];
+        pmModel *model = pmSourceGetModel (NULL, Ri);
+
+        x = model->params->data.F32[PM_PAR_XPOS];
+        y = model->params->data.F32[PM_PAR_YPOS];
+
+        psImageKeepCircle (Ri->mask, x, y, model->radiusFit, "OR", PM_MASK_MARK);
+        pmSourceChisq (model, Ri->pixels, Ri->mask, Ri->weight);
+        psImageKeepCircle (Ri->mask, x, y, model->radiusFit, "AND", PS_NOT_U8(PM_MASK_MARK));
+    }
+
+    psFree (index);
+    psFree (sparse);
+    psFree (fitSources);
+    psFree (norm);
+    psFree (skyfit);
+    psFree (errors);
+    psFree (border);
+
+    psLogMsg ("psphot.ensemble", PS_LOG_INFO, "measure ensemble of PSFs: %f sec\n", psTimerMark ("psphot"));
+    return true;
+}
+
+// calculate the weight terms for the sky fit component of the matrix
+static bool SetBorderT (psSparseBorder *border, pmReadout *readout, psArray *refSources, psArray *fitSources, psVector *index, bool constant_weights, int SKY_FIT_ORDER) {
+
+    // 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", PM_MASK_MARK);
+
+    // turn off MARK for all object pixels
+    // we must use the refSources here since their pixels point at the science image
+    for (int i = 0; i < fitSources->n; i++) {
+        int N = index->data.U32[i];
+        pmSource *refSource = refSources->data[N];
+	pmModel *model = pmSourceGetModel (NULL, refSource);
+	if (model == NULL) continue;
+        float x = model->params->data.F32[PM_PAR_XPOS];
+        float y = model->params->data.F32[PM_PAR_YPOS];
+	psImageMaskCircle (refSource->mask, x, y, model->radiusFit, "AND", PS_NOT_U8(PM_MASK_MARK));
+    }	
+
+    // accumulate the image statistics from the masked regions
+    psF32 **image = readout->image->data.F32;
+    psF32 **weight = readout->weight->data.F32;
+    psU8 **mask = readout->mask->data.U8;
+
+    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 = weight[j][i];
+	    }
+	    f = image[j][i];
+	    w   += 1/wt;
+	    fo  += f/wt;
+	    if (SKY_FIT_ORDER == 0) continue;
+
+	    xc = i + col0;
+	    yc = j + row0;
+	    x  +=    xc/wt;
+	    y  +=    yc/wt;
+	    x2 += xc*xc/wt;
+	    xy += xc*yc/wt;
+	    y2 += yc*yc/wt;
+	    fx +=  f*xc/wt;
+	    fy +=  f*yc/wt;
+	}
+    }
+
+    // turn off MARK for all image pixels
+    psImageMaskRegion (readout->mask, fullArray, "AND", PS_NOT_U8(PM_MASK_MARK));
+
+    // set the Border T elements
+    psSparseBorderElementG (border, 0, fo);
+    psSparseBorderElementT (border, 0, 0, w);
+    if (SKY_FIT_ORDER > 0) {
+	psSparseBorderElementG (border, 0, fx);
+	psSparseBorderElementG (border, 0, fy);
+	psSparseBorderElementT (border, 1, 0, x);
+	psSparseBorderElementT (border, 2, 0, y);
+	psSparseBorderElementT (border, 0, 1, x);
+	psSparseBorderElementT (border, 1, 1, x2);
+	psSparseBorderElementT (border, 2, 1, xy);
+	psSparseBorderElementT (border, 0, 2, y);
+	psSparseBorderElementT (border, 1, 2, xy);
+	psSparseBorderElementT (border, 2, 2, y2);
+    }    
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotErrorCodes.c.in
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotErrorCodes.c.in	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotErrorCodes.c.in	(revision 15358)
@@ -0,0 +1,25 @@
+/*
+ * The line
+    { PSPHOT_ERR_$X{ErrorCode}, "$X{ErrorDescription}"},
+ * (without the Xs)
+ * will be replaced by values from errorCodes.dat
+ */
+#include "pslib.h"
+#include "psphotErrorCodes.h"
+
+void psphotErrorRegister(void)
+{
+    static psErrorDescription errors[] = {
+       { PSPHOT_ERR_BASE, "First value we use; lower values belong to psLib" },
+       { PSPHOT_ERR_${ErrorCode}, "${ErrorDescription}"},
+    };
+    static int nerror = PSPHOT_ERR_NERROR - PSPHOT_ERR_BASE; // number of values in enum
+
+    for (int i = 0; i < nerror; i++) {
+       psErrorDescription *tmp = psAlloc(sizeof(psErrorDescription));
+       *tmp = errors[i];
+       psErrorRegister(tmp, 1);
+       psFree(tmp);			/* it's on the internal list */
+    }
+    nerror = 0;			                // don't register more than once
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotErrorCodes.dat
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotErrorCodes.dat	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotErrorCodes.dat	(revision 15358)
@@ -0,0 +1,17 @@
+#
+# This file is used to generate psphotErrorClasses.h
+#
+UNKNOWN			Unknown PM error code
+NOT_IMPLEMENTED		Desired feature is not yet implemented
+FITS			Problem in FITS I/O
+FITS_WCS		Error interpreting FITS WCS information
+PHOTOM			Problem in photometry
+PSF			Problem in PSF
+APERTURE		Problem with aperture photometry
+SKY			Problem in sky determination
+# these errors correspond to standard exit conditions
+ARGUMENTS               Incorrect arguments
+SYS                     System error
+CONFIG                  Problem in configure files
+PROG                    Programming error
+DATA                    invalid data
Index: /branches/eam_branch_20071023/psphot/src/psphotErrorCodes.h.in
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotErrorCodes.h.in	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotErrorCodes.h.in	(revision 15358)
@@ -0,0 +1,18 @@
+#if !defined(PSPHOT_ERROR_CODES_H)
+#define PSPHOT_ERROR_CODES_H
+/*
+ * The line
+ *  PSPHOT_ERR_$X{ErrorCode},
+ * (without the X)
+ *
+ * will be replaced by values from errorCodes.dat
+ */
+typedef enum {
+    PSPHOT_ERR_BASE = 1300,
+    PSPHOT_ERR_${ErrorCode},
+    PSPHOT_ERR_NERROR
+} psphotErrorCode;
+
+void psphotErrorRegister(void);
+
+#endif
Index: /branches/eam_branch_20071023/psphot/src/psphotEvalFLT.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotEvalFLT.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotEvalFLT.c	(revision 15358)
@@ -0,0 +1,47 @@
+# include "psphotInternal.h"
+
+bool psphotEvalEXT (pmSource *source, pmModel *model)
+{ 
+    int keep;
+
+    // do we actually have a valid EXT model?
+    if (model == NULL) {
+	source->mode &= ~PM_SOURCE_MODE_FITTED;
+	return false;
+    }
+
+    // was the model actually fitted?
+    if (!(model->flags & PM_MODEL_STATUS_FITTED)) {
+	source->mode &= ~PM_SOURCE_MODE_FITTED; 
+	return false;
+    }
+    // did the model fit fail for one or another reason?
+    if (model->flags & (PM_MODEL_STATUS_BADARGS | 
+			PM_MODEL_STATUS_NONCONVERGE | 
+			PM_MODEL_STATUS_OFFIMAGE)) {
+	source->mode |= PM_SOURCE_MODE_FAIL;
+	psLogMsg ("psphot", 5, "EXT fail fit\n");
+	return false;
+    }
+
+    // unless we prove otherwise, this object is extended
+    source->type = PM_SOURCE_TYPE_EXTENDED;
+
+    // the following source->mode information pertains to modelEXT:
+    source->mode |= PM_SOURCE_MODE_EXTMODEL;
+
+    // if the object has a fitted peak below 0, the fit did not converge cleanly
+    // XXX this limit is fairly arbitrary, and must be > the value is the model limits
+    if (model->params->data.F32[PM_PAR_I0] <= 0.02) {
+	source->mode |= PM_SOURCE_MODE_FAIL;
+	return false;
+    } 
+
+    keep = model->modelFitStatus(model);
+    if (keep) return true;
+
+    // poor-quality fit; only keep if nothing else works...
+    psLogMsg ("psphot", 5, "EXT poor fit\n");
+    source->mode |= PM_SOURCE_MODE_POOR;
+    return false;
+}	
Index: /branches/eam_branch_20071023/psphot/src/psphotEvalPSF.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotEvalPSF.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotEvalPSF.c	(revision 15358)
@@ -0,0 +1,238 @@
+# include "psphotInternal.h"
+
+// given a pmSource which has been fitted using modelPSF, evaluate the
+// resulting fit: did the fit succeed? is this object PSF-like? is this object 
+// extended?  return status is TRUE for a valid PSF, FALSE otherwise.
+
+// identify objects consistent with PSF shape/magnitude distribution
+// we expect dparams[PM_PAR_SXX],dparams[PM_PAR_SXX] to have a scatter of:
+// sigma_x / (S/N)
+
+// any objects which is consistent with the PSF should have 
+// abs(dparams[PM_PAR_SXX]) < N * dsxLine(mag) & abs(dparams[PM_PAR_SYY]) < N * dsyLine(mag)
+// this includes a minimum buffer (DS) for the brighter objects
+
+// EAM : 2006.10.20 : I have re-defined params[PM_PAR_SXX] : it is now sqrt(2)*sigma_x, not 1/sigma_x
+
+// saturated stars should fall outside (larger), but have peaks above SATURATION
+// extended sources should be larger, cosmic rays smaller
+// we also reject objects with S/N too low or ChiSquare to high
+
+// floor for DS value 
+// XXX EAM : add to configuration?
+
+static float SATURATION;
+static float PSF_MIN_SN;
+static float PSF_MIN_DS;
+static float PSF_MAX_CHI;
+static float PSF_SHAPE_NSIGMA;
+static float PSF_DSX_MEAN;
+static float PSF_DSY_MEAN;
+static float PSF_DSX_STDEV;
+static float PSF_DSY_STDEV;
+
+bool psphotInitLimitsPSF (psMetadata *recipe, pmReadout *readout) {
+
+    bool status;
+
+    // XXX do we need to set this differently from the value used to mark saturated pixels?
+    pmCell *cell     = readout->parent;
+    SATURATION       = psMetadataLookupF32 (&status, cell->concepts, "CELL.SATURATION");
+    PSF_MIN_SN       = psMetadataLookupF32 (&status, recipe, "PSF_MIN_SN");
+    PSF_MAX_CHI      = psMetadataLookupF32 (&status, recipe, "PSF_MAX_CHI");
+    PSF_SHAPE_NSIGMA = psMetadataLookupF32 (&status, recipe, "PSF_SHAPE_NSIGMA");
+    PSF_MIN_DS       = psMetadataLookupF32 (&status, recipe, "PSF_MIN_DS");
+    if (!status) PSF_MIN_DS = 0.01;
+
+    PSF_DSX_MEAN     = psMetadataLookupF32 (&status, recipe, "DSX_MEAN");
+    PSF_DSY_MEAN     = psMetadataLookupF32 (&status, recipe, "DSY_MEAN");
+    PSF_DSX_STDEV    = psMetadataLookupF32 (&status, recipe, "DSX_STDV");
+    PSF_DSY_STDEV    = psMetadataLookupF32 (&status, recipe, "DSY_STDV");
+
+    return true;
+}
+
+// examine the model->status, fit parameters, etc and decide if the model succeeded
+// set the source->type and source->mode appropriately
+bool psphotEvalPSF (pmSource *source, pmModel *model) { 
+
+    int keep;
+    float dSX, dSY, SX, SY, SN;
+    float nSx, nSy, Chi;
+
+    // do we actually have a valid PSF model?
+    if (model == NULL) {
+	source->mode &= ~PM_SOURCE_MODE_FITTED;
+	return false;
+    }
+
+    // was the model actually fitted?
+    if (!(model->flags & PM_MODEL_STATUS_FITTED)) {
+	source->mode &= ~PM_SOURCE_MODE_FITTED; 
+	if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+	    psTrace ("psphot", 5, "satstar was not fitted");
+	}
+	return false;
+    }
+    // did the model fit fail for one or another reason?
+    if (model->flags & (PM_MODEL_STATUS_BADARGS | 
+			PM_MODEL_STATUS_NONCONVERGE | 
+			PM_MODEL_STATUS_OFFIMAGE)) {
+	source->mode |= PM_SOURCE_MODE_FAIL;
+	if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+	    psTrace ("psphot", 5, "satstar failed fit");
+	}
+	return false;
+    }
+
+    // unless we prove otherwise, this object is a star.
+    source->type = PM_SOURCE_TYPE_STAR;
+
+    // the following source->mode information pertains to modelPSF:
+    source->mode |= PM_SOURCE_MODE_PSFMODEL;
+
+    // if the object has fitted peak above saturation, label as SATSTAR
+    // this is a valid PSF object, but ignore the other quality tests
+    // remember: fit does not use saturated pixels (masked)
+    // XXX no extended object can saturate and stay extended...
+    if (model->params->data.F32[PM_PAR_I0] >= SATURATION) {
+	if (source->mode & PM_SOURCE_MODE_PSFSTAR) {
+	    psLogMsg ("psphot", 5, "PSFSTAR marked SATSTAR\n");
+	}
+	source->mode |=  PM_SOURCE_MODE_SATSTAR;
+	return true;
+    } 
+
+    // if the object has a fitted peak below 0.02, the source is not viable
+    // perhaps the fit did not converge cleanly
+    if (model->params->data.F32[PM_PAR_I0] <= 0.02) {
+	source->mode |= PM_SOURCE_MODE_FAIL;
+	if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+	    psTrace ("psphot", 5, "satstar failed fit (peak below 0)");
+	}
+	return false;
+    } 
+
+    // if the source was predicted to be a SATSTAR, but it fitted below saturation, 
+    // make a note to the user
+    if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+	psLogMsg ("psphot", 5, "SATSTAR marked normal (fitted peak below saturation)\n");
+	source->mode &= ~PM_SOURCE_MODE_SATSTAR;
+    }
+
+    SN  = model->params->data.F32[PM_PAR_I0]/model->dparams->data.F32[PM_PAR_I0];
+    SX  = model->params->data.F32[PM_PAR_SXX];
+    SY  = model->params->data.F32[PM_PAR_SYY];
+    dSX = model->dparams->data.F32[PM_PAR_SXX];
+    dSY = model->dparams->data.F32[PM_PAR_SYY];
+    Chi = model->chisqNorm / model->nDOF;
+
+    // swing of sigma_x,y in sigmas
+    nSx = (dSX - PSF_DSX_MEAN) / (PSF_DSX_STDEV * PS_MAX (PSF_MIN_DS, (SX / SN)));
+    nSy = (dSY - PSF_DSY_MEAN) / (PSF_DSY_STDEV * PS_MAX (PSF_MIN_DS, (SY / SN)));
+
+    // assign PM_SOURCE_MODE_GOODSTAR to bright objects within PSF region of dparams[]
+    keep = TRUE;
+    keep &= (fabs(nSx) < PSF_SHAPE_NSIGMA);
+    keep &= (fabs(nSy) < PSF_SHAPE_NSIGMA);
+    keep &= (SN > PSF_MIN_SN);
+    keep &= (Chi < PSF_MAX_CHI);
+
+    if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+	psTrace ("psphot", 5, "satstar fit results: %f, %f  %d :  %f %f : %f %f : %f %f\n", 
+		     model->params->data.F32[PM_PAR_XPOS], model->params->data.F32[PM_PAR_YPOS], keep,
+		 dSX, nSx, dSY, nSy, SN, Chi);
+    }
+
+    if (keep) {
+	if (source->mode & PM_SOURCE_MODE_PSFSTAR) {
+	    psTrace ("psphot", 7, "PSFSTAR kept (%f, %f  :  %f %f : %f %f : %f %f)\n", 
+		     model->params->data.F32[PM_PAR_XPOS], model->params->data.F32[PM_PAR_YPOS], dSX, nSx, dSY, nSy, SN, Chi);
+	}
+	return true;
+    }
+
+    // this source is not a star, warn if it was a PSFSTAR
+    if (source->mode & PM_SOURCE_MODE_PSFSTAR) {
+	psTrace ("psphot", 5, "PSFSTAR demoted based on fit quality   (%f, %f  :  %f %f : %f %f : %f %f)\n", 
+		  model->params->data.F32[PM_PAR_XPOS], model->params->data.F32[PM_PAR_YPOS], dSX, nSx, dSY, nSy, SN, Chi);
+    } else {
+	psTrace ("psphot", 5, "fails PSF fit (%f, %f  :  %f %f : %f %f : %f %f)\n", 
+		  model->params->data.F32[PM_PAR_XPOS], model->params->data.F32[PM_PAR_YPOS], dSX, nSx, dSY, nSy, SN, Chi);
+    }
+
+    // object appears to be small, suspected defect.  this is a weak indication for a defect: a
+    // large positive or negative swing can also come from the bright sources for which we are 
+    // more sensitive to the wings than for the faint sources.  
+    // XXX allow -NSIGMA and +NSIGMA to have different values?
+    if ((nSx <= -PSF_SHAPE_NSIGMA) || (nSy <= -PSF_SHAPE_NSIGMA)) {
+	source->type = PM_SOURCE_TYPE_DEFECT;
+	return false;
+    }
+
+    // object appears to be large, suspected extended source
+    if ((nSx >= PSF_SHAPE_NSIGMA) || (nSy >= PSF_SHAPE_NSIGMA)) {
+	source->type = PM_SOURCE_TYPE_EXTENDED;
+	return false;
+    }
+
+    // poor-quality fit; only keep if nothing else works...
+    source->mode |= PM_SOURCE_MODE_POOR;
+    return false;
+}	
+
+// examine the model->status, fit parameters, etc and decide if the model succeeded
+// set the source->type and source->mode appropriately
+bool psphotEvalDBL (pmSource *source, pmModel *model) { 
+
+    // do we actually have a valid PSF model?
+    if (model == NULL) {
+	source->mode &= ~PM_SOURCE_MODE_FITTED;
+	return false;
+    }
+
+    // was the model actually fitted?
+    if (!(model->flags & PM_MODEL_STATUS_FITTED)) {
+	source->mode &= ~PM_SOURCE_MODE_FITTED; 
+	return false;
+    }
+    // did the model fit fail for one or another reason?
+    if (model->flags & (PM_MODEL_STATUS_BADARGS | 
+			PM_MODEL_STATUS_NONCONVERGE | 
+			PM_MODEL_STATUS_OFFIMAGE)) {
+	source->mode |= PM_SOURCE_MODE_FAIL;
+	return false;
+    }
+
+    // unless we prove otherwise, this object is a star.
+    source->type = PM_SOURCE_TYPE_STAR;
+
+    // the following source->mode information pertains to modelPSF:
+    source->mode |= PM_SOURCE_MODE_PSFMODEL;
+
+    // if the object has fitted peak above saturation, label as SATSTAR
+    // this is a valid PSF object, but ignore the other quality tests
+    // remember: fit does not use saturated pixels (masked)
+    // XXX no extended object can saturate and stay extended...
+    if (model->params->data.F32[PM_PAR_I0] >= SATURATION) {
+	if (source->mode & PM_SOURCE_MODE_PSFSTAR) {
+	    psLogMsg ("psphot", 5, "PSFSTAR marked SATSTAR\n");
+	}
+	source->mode |=  PM_SOURCE_MODE_SATSTAR;
+	return true;
+    } 
+
+    // if the object has a fitted peak below 0, the fit did not converge cleanly
+    if (model->params->data.F32[PM_PAR_I0] <= 0) {
+	source->mode |= PM_SOURCE_MODE_FAIL;
+	return false;
+    } 
+
+    // if the source was predicted to be a SATSTAR, but it fitted below saturation, 
+    // make a note to the user
+    if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+	psLogMsg ("psphot", 5, "SATSTAR marked normal (fitted peak below saturation)\n");
+	source->mode &= ~PM_SOURCE_MODE_SATSTAR;
+    }
+    return true;
+}	
Index: /branches/eam_branch_20071023/psphot/src/psphotExtendedSources.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotExtendedSources.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotExtendedSources.c	(revision 15358)
@@ -0,0 +1,113 @@
+# include "psphot.h"
+
+bool psphotExtendedSources (pmReadout *readout, psArray *sources, psMetadata *recipe, psMaskType maskVal) {
+
+    bool status;
+    int Next = 0;
+    int Npsf = 0;
+
+    // S/N limit to perform full non-linear fits
+    if (!psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_FITS")) {
+	psLogMsg ("psphot", PS_LOG_INFO, "skipping extended source measurements\n");
+	return true;
+    }
+
+    // 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");
+
+    // S/N limit to perform full non-linear fits
+    float SN_LIM = psMetadataLookupF32 (&status, recipe, "EXTENDED_SOURCE_SN_LIM");
+
+    // which extended source analyses should we perform?
+    bool doPSFConvModel = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_PSF_CONVOLVED_MODEL");
+    bool doPetrosian    = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_PETROSIAN");
+    bool doIsophotal    = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_ISOPHOTAL");
+    bool doAnnuli       = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_ANNULI");
+    bool doKron         = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_KRON");
+
+    // source analysis is done in S/N order (brightest first)
+    sources = psArraySort (sources, pmSourceSortBySN);
+
+    // XXX some init functions for the extended source recipe options?
+
+    // 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;
+
+        // limit selection to some SN limit
+        assert (source->peak); // how can a source not have a peak?
+        if (source->peak->SN < SN_LIM) continue;
+
+        // XXX this should use peak?
+        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;
+
+        // if model is NULL, we don't have a starting guess
+        if (source->modelEXT == NULL) continue;
+
+        // replace object in image
+        if (source->mode & PM_SOURCE_MODE_SUBTRACTED) {
+            pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
+        }
+        Next ++;
+
+	if (doPSFConvModel && !psphotPSFConvModel (source, recipe, maskVal)) {
+	    psError(PSPHOT_ERR_UNKNOWN, false, "failure in PSF Convolved Model fit");
+	    return false;
+	} else {
+	    pmSourceCacheModel (source, maskVal); // XXX put this in the source model function?
+	    psTrace ("psphot", 5, "psf-convolved model for source at %7.1f, %7.1f", source->moments->x, source->moments->y);
+            Npsf ++;
+	}
+
+	if (doPetrosian && !psphotPetrosian (source, recipe, maskVal)) {
+	    psError(PSPHOT_ERR_UNKNOWN, false, "failure in Petrosian analysis");
+	    return false;
+	} else {
+	    psTrace ("psphot", 5, "psf-convolved model for source at %7.1f, %7.1f", source->moments->x, source->moments->y);
+            Npsf ++;
+	}
+
+	if (doIsophotal && !psphotIsophotal (source, recipe, maskVal)) {
+	    psError(PSPHOT_ERR_UNKNOWN, false, "failure in Isophotal analysis");
+	    return false;
+	} else {
+	    psTrace ("psphot", 5, "psf-convolved model for source at %7.1f, %7.1f", source->moments->x, source->moments->y);
+            Npsf ++;
+	}
+
+	if (doAnnuli && !psphotAnnuli (source, recipe, maskVal)) {
+	    psError(PSPHOT_ERR_UNKNOWN, false, "failure in Annuli analysis");
+	    return false;
+	} else {
+	    psTrace ("psphot", 5, "psf-convolved model for source at %7.1f, %7.1f", source->moments->x, source->moments->y);
+            Npsf ++;
+	}
+
+	if (doKron && !psphotKron (source, recipe, maskVal)) {
+	    psError(PSPHOT_ERR_UNKNOWN, false, "failure in Kron analysis");
+	    return false;
+	} else {
+	    psTrace ("psphot", 5, "psf-convolved model for source at %7.1f, %7.1f", source->moments->x, source->moments->y);
+            Npsf ++;
+	}
+
+        // re-subtract the object, leave local sky
+        pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
+        source->mode |= PM_SOURCE_MODE_SUBTRACTED;
+        source->mode |= PM_SOURCE_MODE_TEMPSUB;
+    }
+
+    psLogMsg ("psphot", PS_LOG_INFO, "extended source analysis: %f sec for %d objects\n", psTimerMark ("psphot"), Next);
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotFakeSources.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotFakeSources.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotFakeSources.c	(revision 15358)
@@ -0,0 +1,30 @@
+# include "psphotInternal.h"
+
+psArray *psphotFakeSources () {
+
+    // psphotUpdateHeader (header, config);
+
+    psArray *sources = psArrayAlloc (50);
+
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = pmSourceAlloc ();
+        source->moments = pmMomentsAlloc ();
+        source->moments->x = 10;
+        source->moments->y = 10;
+        source->moments->Sx = 1;
+        source->moments->Sy = 1;
+        source->moments->Sxy = 0;
+        source->moments->Sum = 1000;
+        source->moments->Peak = 100;
+        source->moments->Sky = 10;
+        source->moments->nPixels = 10;
+
+        source->peak = pmPeakAlloc (10, 10, 0, 0);
+        source->type = PM_SOURCE_TYPE_STAR;
+
+        pmModelType modelType = pmModelClassGetType ("PS_MODEL_QGAUSS");
+        source->modelPSF = pmSourceModelGuess (source, modelType);
+        sources->data[i] = source;
+    }
+    return sources;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotFindPeaks.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotFindPeaks.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotFindPeaks.c	(revision 15358)
@@ -0,0 +1,195 @@
+# include "psphotInternal.h"
+
+// XXX let's make a better distinction between 'pass' and needing/having a PSF.
+// In this function, we smooth the image, then search for the peaks
+psArray *psphotFindPeaks (pmReadout *readout, psMetadata *recipe,
+                          bool returnFootprints,
+                          const int pass, psMaskType maskVal) {
+
+    float SIGMA_SMTH, NSIGMA_SMTH, NSIGMA_PEAK;
+    bool  status = false;
+
+    // smooth the image and weight map
+    psTimerStart ("psphot");
+
+    // XXX if we have been supplied a PSF, we can use that to set the FWHM_X,FWHM_Y values
+    if (pass == 1) {
+        SIGMA_SMTH  = psMetadataLookupF32 (&status, recipe, "PEAKS_SMOOTH_SIGMA");
+        NSIGMA_SMTH = psMetadataLookupF32 (&status, recipe, "PEAKS_SMOOTH_NSIGMA");
+    } else {
+        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) {
+            psError(PSPHOT_ERR_CONFIG, false, "FWHM_X or FWHM_Y not defined");
+            return false;
+        }
+        SIGMA_SMTH  = 0.5*(FWHM_X + FWHM_Y) / (2.0*sqrt(2.0*log(2.0)));
+        NSIGMA_SMTH = psMetadataLookupF32 (&status, recipe, "PEAKS_SMOOTH_NSIGMA");
+    }
+
+    // smooth the image, applying the mask as we go
+    psImage *smooth_im = psImageCopy (NULL, readout->image, PS_TYPE_F32);
+    psImageSmoothMaskF32 (smooth_im, readout->mask, maskVal, SIGMA_SMTH, NSIGMA_SMTH);
+    psLogMsg ("psphot", PS_LOG_MINUTIA, "smooth image: %f sec\n", psTimerMark ("psphot"));
+
+    // smooth the weight, applying the mask as we go
+    psImage *smooth_wt = psImageCopy (NULL, readout->weight, PS_TYPE_F32);
+    psImageSmoothMaskF32 (smooth_wt, readout->mask, maskVal, SIGMA_SMTH/sqrt(2), NSIGMA_SMTH);
+    psLogMsg ("psphot", PS_LOG_MINUTIA, "smooth weight: %f sec\n", psTimerMark ("psphot"));
+
+    psImage *mask = readout->mask;
+
+    // optionally save example images under trace
+    if (psTraceGetLevel("psphot") > 5) {
+        char name[64];
+        sprintf (name, "imsmooth.v%d.fits", pass);
+        psphotSaveImage (NULL, smooth_im, name);
+        sprintf (name, "wtsmooth.v%d.fits", pass);
+        psphotSaveImage (NULL, smooth_wt, name);
+    }
+
+    // build the significance image on top of smooth_im
+    for (int j = 0; j < smooth_im->numRows; j++) {
+        for (int i = 0; i < smooth_im->numCols; i++) {
+            float value = smooth_im->data.F32[j][i];
+            if (value < 0 || smooth_wt->data.F32[j][i] <= 0 || (mask->data.U8[j][i] & maskVal)) {
+                smooth_im->data.F32[j][i] = 0.0;
+            } else {
+                smooth_im->data.F32[j][i] = PS_SQR(value) / smooth_wt->data.F32[j][i];
+            }
+        }
+    }
+    psLogMsg ("psphot", PS_LOG_INFO, "built smoothed signficance image: %f sec\n", psTimerMark ("psphot"));
+
+    // optionally save example images under trace
+    if (psTraceGetLevel("psphot") > 5) {
+        char name[64];
+        sprintf (name, "snsmooth.v%d.fits", pass);
+        psphotSaveImage (NULL, smooth_im, name);
+    }
+
+    psTimerStart ("psphot");
+    // set peak threshold
+
+    // signal/noise limit for the detected peaks
+    if (pass == 1) {
+        NSIGMA_PEAK = psMetadataLookupF32 (&status, recipe, "PEAKS_NSIGMA_LIMIT");
+    } else {
+        NSIGMA_PEAK = psMetadataLookupF32 (&status, recipe, "PEAKS_NSIGMA_LIMIT_2");
+    }
+
+    // we need to define the threshold based on the value of NSIGMA_PEAK and the applied smoothing
+    // gaussian SIGMA.  a peak in the significance image has an effective S/N for faint sources
+    // of (S = Io*2pi*sigma_eff^2) / sqrt(Var), where sigma_eff^2 = sigma_obs^2 + sigma_sm^2.
+    // the smoothing of the varience map does not affect the faint-source S/N since the noise
+    // is constant under a faint source.
+
+    // if sigma_sm = sigma_obs, then sigma_eff^2 = 2 sigma_sm^2. in this case, the threshold in
+    // terms of smooth_im peak counts Io, for a desired S/N limit corresponds to
+    // S/N = sqrt(Io)*4*pi*sigma_sm^2
+    // thus, the threshold is:
+    float effArea = 4.0*M_PI*PS_SQR(SIGMA_SMTH);
+    if (effArea < 1) {
+        effArea = 1;                    // never less than a pixel
+    }
+    float threshold = PS_SQR(NSIGMA_PEAK) / effArea;
+
+    // find the peaks in the smoothed image
+    psArray *peaks = pmPeaksInImage (smooth_im, threshold);
+    if (peaks == NULL) {
+        // XXX this may also be due to a programming or config error
+        // 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;
+    }
+
+    // correct the peak values to S/N = sqrt(value*effArea)
+    // get the peak flux from the unsmoothed image
+    // the peak pixel coords are guaranteed to be on the image
+    int row0 = readout->image->row0;
+    int col0 = readout->image->col0;
+    for (int i = 0; i < peaks->n; i++) {
+        pmPeak *peak = peaks->data[i];
+        peak->SN = sqrt(peak->value*effArea);
+        peak->flux = readout->image->data.F32[peak->y-row0][peak->x-col0];
+    }
+
+    // limit the total number of returned peak as specified
+    if (pass == 1) {
+        psArraySort (peaks, pmPeakSortBySN);
+        int NMAX = psMetadataLookupS32 (&status, recipe, "PEAKS_NMAX");
+        if (NMAX && (peaks->n > NMAX)) {
+            psArray *tmpPeaks = psArrayAllocEmpty (NMAX);
+            for (int i = 0; i < NMAX; i++) {
+                psArrayAdd (tmpPeaks, 100, peaks->data[i]);
+            }
+            psFree (peaks);
+            peaks = tmpPeaks;
+        }
+    }
+
+    // optional dump of all peak data
+    char *output = psMetadataLookupStr (&status, recipe, "PEAKS_OUTPUT_FILE");
+    if (status && (output != NULL) && (output[0])) {
+        pmPeaksWriteText (peaks, output);
+    }
+    psLogMsg ("psphot", PS_LOG_INFO, "%ld peaks: %f sec\n", peaks->n, psTimerMark ("psphot"));
+
+    // If they asked us to return a set of pmFootprints, not a raw pmPeak list,
+    // go ahead and find the footprints, and assign them their peaks.
+    //
+    // N.b. We're not culling this list; call pmFootprintCullPeaks if you
+    // want to do that
+    //
+    if (returnFootprints) {     // We want an array of pmFootprint, not pmPeak
+        int npixMin = psMetadataLookupS32(&status, recipe, "FOOTPRINT_NPIXMIN");
+        if (!status) {
+            npixMin = 1;
+        }
+        float FOOTPRINT_NSIGMA_LIMIT =
+            psMetadataLookupS32(&status, recipe,
+                                (pass == 1) ? "FOOTPRINT_NSIGMA_LIMIT" : "FOOTPRINT_NSIGMA_LIMIT_2");
+        if (!status) {
+            FOOTPRINT_NSIGMA_LIMIT = NSIGMA_PEAK;
+        }
+        threshold = PS_SQR(FOOTPRINT_NSIGMA_LIMIT)/effArea;
+
+        psArray *footprints = pmFindFootprints(smooth_im, threshold, npixMin);
+        pmPeaksAssignToFootprints(footprints, peaks);
+
+        psFree(peaks);
+        peaks = footprints;             // well, you know what I mean
+    }
+
+    psFree (smooth_im);
+    psFree (smooth_wt);
+
+    return (peaks);
+}
+
+
+/************************************************************************************************************/
+/*
+ * Cull a set of peaks contained in a psArray of pmFootprints
+ */
+psErrorCode
+psphotCullPeaks(const psImage *image,   // the image wherein lives the footprint
+                const psImage *weight,  // corresponding variance image
+                const psMetadata *recipe,
+                psArray *footprints) {  // array of pmFootprints
+    bool status = false;
+    float nsigma_delta = psMetadataLookupF32(&status, recipe, "FOOTPRINT_CULL_NSIGMA_DELTA");
+    if (!status) {
+        nsigma_delta = 0; // min.
+    }
+    float nsigma_min = psMetadataLookupF32(&status, recipe, "FOOTPRINT_CULL_NSIGMA_MIN");
+    if (!status) {
+        nsigma_min = 0;
+    }
+    const float skyStdev = psMetadataLookupF32(NULL, recipe, "SKY_STDEV");
+
+    return pmFootprintArrayCullPeaks(image, weight, footprints,
+                                     nsigma_delta, nsigma_min*skyStdev);
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotFitSet.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotFitSet.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotFitSet.c	(revision 15358)
@@ -0,0 +1,48 @@
+# include "psphotInternal.h"
+
+// This is only used by psphotModelTest.c
+bool psphotFitSet (pmSource *source, pmModel *oneModel, char *fitset, pmSourceFitMode mode, psMaskType maskVal) {
+
+    double x, y, Io;
+
+    FILE *f = fopen (fitset, "r");
+    if (f == NULL) return false;
+
+    psArray *modelSet = psArrayAllocEmpty (16);
+
+    while (fscanf (f, "%lf %lf %lf", &x, &y, &Io) == 3) {
+        pmModel *model = pmModelAlloc (oneModel->type);
+
+        for (psS32 i = 0; i < model->params->n; i++) {
+            model->params->data.F32[i] = oneModel->params->data.F32[i];
+            model->dparams->data.F32[i] = oneModel->dparams->data.F32[i];
+        }
+        model->params->data.F32[1] = Io;
+        model->params->data.F32[2] = x;
+        model->params->data.F32[3] = y;
+        psArrayAdd (modelSet, 16, model);
+    }
+
+    // XXX pmSourceFitSet must cache the modelFlux?
+    pmSourceFitSet (source, modelSet, mode, maskVal);
+
+    // write out positive object
+    psphotSaveImage (NULL, source->pixels, "object.fits");
+
+    // subtract object, leave local sky
+    for (int i = 0; i < modelSet->n; i++) {
+        pmModel *model = modelSet->data[i];
+        pmModelSub (source->pixels, source->maskObj, model, PM_MODEL_OP_FULL, maskVal);
+
+        fprintf (stderr, "output parameters (obj %d):\n", i);
+        for (int n = 0; n < model->params->n; n++) {
+            fprintf (stderr, "%d : %f\n", n, model->params->data.F32[n]);
+        }
+    }
+
+    // write out
+    psphotSaveImage (NULL, source->pixels, "resid.fits");
+    psphotSaveImage (NULL, source->maskObj, "mask.fits");
+    return true;
+}
+
Index: /branches/eam_branch_20071023/psphot/src/psphotFitSourcesLinear.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotFitSourcesLinear.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotFitSourcesLinear.c	(revision 15358)
@@ -0,0 +1,288 @@
+# include "psphotInternal.h"
+
+// 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
+
+// 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, int SKY_FIT_ORDER);
+
+bool psphotFitSourcesLinear (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, bool final, psMaskType maskVal) {
+
+    bool status;
+    float x;
+    float y;
+    float f;
+    // float r;
+
+    psTimerStart ("psphot");
+
+    // source analysis is done in spatial order
+    sources = psArraySort (sources, pmSourceSortByY);
+
+    // storage array for fitSources
+    psArray *fitSources = psArrayAllocEmpty (sources->n);
+
+    // option to limit analysis to a specific region
+    char *region = psMetadataLookupStr (&status, recipe, "ANALYSIS_REGION");
+    psRegion AnalysisRegion = psRegionFromString (region);
+    AnalysisRegion = psRegionForImage (readout->image, AnalysisRegion);
+    if (psRegionIsNaN (AnalysisRegion)) psAbort("analysis region mis-defined");
+
+    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");
+    }
+    int SKY_FIT_ORDER = psMetadataLookupS32(&status, recipe, "SKY_FIT_ORDER");
+    if (!status) {
+        SKY_FIT_ORDER = 0;
+    }
+    bool SKY_FIT_LINEAR = psMetadataLookupBool(&status, recipe, "SKY_FIT_LINEAR");
+    if (!status) {
+        SKY_FIT_LINEAR = false;
+    }
+
+    // select the sources which will be used for the fitting analysis
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+
+        // 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->type == PM_SOURCE_TYPE_STAR &&
+            source->mode & PM_SOURCE_MODE_SATSTAR) continue;
+        if (final) {
+            if (source->mode &  PM_SOURCE_MODE_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;
+	}
+
+        // save the original coords
+        x = source->peak->xf;
+        y = source->peak->yf;
+
+        // is the source in the region of interest?
+        if (x < AnalysisRegion.x0) continue;
+        if (y < AnalysisRegion.y0) continue;
+        if (x > AnalysisRegion.x1) continue;
+        if (y > AnalysisRegion.y1) continue;
+
+        psArrayAdd (fitSources, 100, source);
+    }
+    psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "built fitSources: %f (%ld objects)\n", psTimerMark ("psphot"), sources->n);
+
+    // vectors to store stats for each object
+    // psVector *weight = psVectorAlloc (fitSources->n, PS_TYPE_F32);
+    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
+    psSparse *sparse = psSparseAlloc (fitSources->n, 100);
+    int nBorder = (SKY_FIT_ORDER == 0) ? 1 : 3;
+    psSparseBorder *border = psSparseBorderAlloc (sparse, nBorder);
+
+    // fill out the sparse matrix elements and border elements (B)
+    // SRCi is the current source of interest
+    // SRCj is a possibly overlapping source
+    for (int i = 0; i < fitSources->n; i++) {
+        pmSource *SRCi = fitSources->data[i];
+
+        // diagonal elements of the sparse matrix (auto-cross-product)
+        f = pmSourceModelDotModel (SRCi, SRCi, CONSTANT_PHOTOMETRIC_WEIGHTS);
+        psSparseMatrixElement (sparse, i, i, f);
+
+        // the formal error depends on the weighting scheme
+        if (CONSTANT_PHOTOMETRIC_WEIGHTS) {
+            float var = pmSourceModelDotModel (SRCi, SRCi, false);
+            errors->data.F32[i] = 1.0 / sqrt(var);
+        } else {
+            errors->data.F32[i] = 1.0 / sqrt(f);
+        }
+
+
+        // find the image x model value
+        f = pmSourceDataDotModel (SRCi, SRCi, CONSTANT_PHOTOMETRIC_WEIGHTS);
+        psSparseVectorElement (sparse, i, f);
+
+        // add the per-source weights (border region)
+        switch (SKY_FIT_ORDER) {
+          case 1:
+            f = pmSourceModelWeight (SRCi, 1, CONSTANT_PHOTOMETRIC_WEIGHTS);
+            psSparseBorderElementB (border, i, 1, f);
+            f = pmSourceModelWeight (SRCi, 2, CONSTANT_PHOTOMETRIC_WEIGHTS);
+            psSparseBorderElementB (border, i, 2, f);
+
+          case 0:
+            f = pmSourceModelWeight (SRCi, 0, CONSTANT_PHOTOMETRIC_WEIGHTS);
+            psSparseBorderElementB (border, i, 0, f);
+            break;
+
+          default:
+            psAbort("Invalid SKY_FIT_ORDER %d\n", SKY_FIT_ORDER);
+            break;
+        }
+
+        // loop over all other stars following this one
+        for (int j = i + 1; j < fitSources->n; j++) {
+            pmSource *SRCj = fitSources->data[j];
+
+            // 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;
+
+            // got an overlap; calculate cross-product and add to output array
+            f = pmSourceModelDotModel (SRCi, SRCj, CONSTANT_PHOTOMETRIC_WEIGHTS);
+            psSparseMatrixElement (sparse, j, i, f);
+        }
+    }
+
+    psSparseResort (sparse);
+    psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "built matrix: %f (%d elements)\n", psTimerMark ("psphot"), sparse->Nelem);
+
+    // set the sky, sky_x, sky_y components of border matrix
+    SetBorderMatrixElements (border, readout, fitSources, CONSTANT_PHOTOMETRIC_WEIGHTS, SKY_FIT_ORDER);
+
+    psSparseConstraint constraint;
+    constraint.paramMin   = 0.0;
+    constraint.paramMax   = 1e8;
+    constraint.paramDelta = 1e8;
+
+    // solve for normalization terms (need include local sky?)
+    psVector *norm = NULL;
+    psVector *skyfit = NULL;
+    if (SKY_FIT_LINEAR) {
+        psSparseBorderSolve (&norm, &skyfit, constraint, border, 5);
+        fprintf (stderr, "skyfit: %f\n", skyfit->data.F32[0]);
+    } else {
+        norm = psSparseSolve (NULL, constraint, sparse, 5);
+        skyfit = NULL;
+    }
+
+    // adjust I0 for fitSources and subtract
+    for (int i = 0; i < fitSources->n; i++) {
+        pmSource *source = fitSources->data[i];
+        pmModel *model = pmSourceGetModel (NULL, source);
+
+        // assign linearly-fitted normalization
+        if (isnan(norm->data.F32[i])) {
+            psAbort("linear fitted source is nan");
+        }
+        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->mode |= PM_SOURCE_MODE_SUBTRACTED;
+        if (!final) source->mode |= PM_SOURCE_MODE_TEMPSUB;
+        // XXX not sure about the use of TEMPSUB
+    }
+
+    // measure chisq for each source
+    for (int i = 0; final && (i < fitSources->n); i++) {
+        pmSource *source = fitSources->data[i];
+        pmModel *model = pmSourceGetModel (NULL, source);
+        pmSourceChisq (model, source->pixels, source->maskObj, source->weight, maskVal);
+    }
+
+    // psFree (index);
+    psFree (sparse);
+    psFree (fitSources);
+    psFree (norm);
+    psFree (skyfit);
+    psFree (errors);
+    psFree (border);
+
+    psLogMsg ("psphot.ensemble", PS_LOG_INFO, "measure ensemble of PSFs: %f sec\n", psTimerMark ("psphot"));
+    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, int SKY_FIT_ORDER) {
+
+    // 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", PM_MASK_MARK);
+
+    // 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_U8(PM_MASK_MARK));
+    }
+
+    // accumulate the image statistics from the masked regions
+    psF32 **image  = readout->image->data.F32;
+    psF32 **weight = readout->weight->data.F32;
+    psU8  **mask   = readout->mask->data.U8;
+
+    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 = weight[j][i];
+            }
+            f = image[j][i];
+            w   += 1/wt;
+            fo  += f/wt;
+            if (SKY_FIT_ORDER == 0) continue;
+
+            xc  = i + col0;
+            yc  = j + row0;
+            x  +=    xc/wt;
+            y  +=    yc/wt;
+            x2 += xc*xc/wt;
+            xy += xc*yc/wt;
+            y2 += yc*yc/wt;
+            fx +=  f*xc/wt;
+            fy +=  f*yc/wt;
+        }
+    }
+
+    // turn off MARK for all image pixels
+    psImageMaskRegion (readout->mask, fullArray, "AND", PS_NOT_U8(PM_MASK_MARK));
+
+    // set the Border T elements
+    psSparseBorderElementG (border, 0, fo);
+    psSparseBorderElementT (border, 0, 0, w);
+    if (SKY_FIT_ORDER > 0) {
+        psSparseBorderElementG (border, 0, fx);
+        psSparseBorderElementG (border, 0, fy);
+        psSparseBorderElementT (border, 1, 0, x);
+        psSparseBorderElementT (border, 2, 0, y);
+        psSparseBorderElementT (border, 0, 1, x);
+        psSparseBorderElementT (border, 1, 1, x2);
+        psSparseBorderElementT (border, 2, 1, xy);
+        psSparseBorderElementT (border, 0, 2, y);
+        psSparseBorderElementT (border, 1, 2, xy);
+        psSparseBorderElementT (border, 2, 2, y2);
+    }
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotGrowthCurve.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotGrowthCurve.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotGrowthCurve.c	(revision 15358)
@@ -0,0 +1,70 @@
+# include "psphotInternal.h"
+
+// XXX we can probably move this into pmGrowthCurve.c
+// XXX need to change the way we grab an image, or else use
+//     the 'center' option
+
+// XXX add a option to turn off the curve-of-growth (ie, make the apMag = fitMag everywhere);
+
+bool psphotGrowthCurve (pmReadout *readout, pmPSF *psf, bool ignore, psMaskType maskVal) {
+
+    // bool status;
+    float xc, yc, dx, dy;
+    float fitMag, apMag;
+    float radius;
+
+    // use the center of the center pixel of the image
+    xc = 0.5*readout->image->numCols + readout->image->col0 + 0.5;
+    yc = 0.5*readout->image->numRows + readout->image->row0 + 0.5;
+    dx = psf->growth->maxRadius + 1;
+    dy = psf->growth->maxRadius + 1;
+
+    // create normalized model object at xc,yc 
+    pmModel *model = pmModelFromPSFforXY (psf, xc, yc, 1.0);
+
+    // measure the fitMag for this model
+    pmSourcePhotometryModel (&fitMag, model);
+    psf->growth->fitMag = fitMag;
+
+    // generate working image for this source
+    psRegion region = {xc - dx, xc + dx, yc - dy, yc + dy};
+    psImage *view = psImageSubset (readout->image, region);
+    psImage *image = psImageCopy (NULL, view, PS_TYPE_F32);
+    psImage *mask = psImageCopy (NULL, view, PS_TYPE_U8);
+    psImageInit (mask, 0);
+
+    // loop over a range of source fluxes
+    // no need to interpolate since we have force the object center
+    // to 0.5, 0.5 above
+    for (int i = 0; i < psf->growth->radius->n; i++) {
+
+        psImageInit (image, 0.0);
+
+        radius = psf->growth->radius->data.F32[i];
+
+        // NOTE: we use pmModelAdd not pmSourceAdd because we are not working with a normal source
+        psImageKeepCircle (mask, xc, yc, radius, "OR", PM_MASK_MARK);
+        pmModelAdd (image, mask, model, PM_MODEL_OP_FULL, maskVal);
+        pmSourcePhotometryAper (&apMag, model, image, mask, maskVal);
+        psImageKeepCircle (mask, xc, yc, radius, "AND", PS_NOT_U8(PM_MASK_MARK));
+
+        // the 'ignore' mode is for testing
+        if (ignore) {
+            psf->growth->apMag->data.F32[i] = fitMag;
+        } else {
+            psf->growth->apMag->data.F32[i] = apMag;
+        }
+    }
+
+    psf->growth->apRef = psVectorInterpolate (psf->growth->radius, psf->growth->apMag, psf->growth->refRadius);
+    psf->growth->apLoss = psf->growth->fitMag - psf->growth->apRef;
+
+    psLogMsg ("psphot.growth", 4, "GrowthCurve : apLoss : %f\n", psf->growth->apLoss);
+
+    psFree (view);
+    psFree (image);
+    psFree (mask);
+    psFree (model);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotGuessModels.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotGuessModels.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotGuessModels.c	(revision 15358)
@@ -0,0 +1,93 @@
+# include "psphotInternal.h"
+
+// A guess for when the moments aren't available
+static pmModel *wildGuess(pmSource *source, // Source for which to guess
+                          pmPSF *psf    // The point-spread function
+    )
+{
+    pmModel *model = pmModelAlloc(psf->type);
+    psF32 *PAR = model->params->data.F32;
+    PAR[PM_PAR_SKY]  = 0;
+    // XXX get this from the image pixels
+    PAR[PM_PAR_I0]   = source->peak->flux;
+    PAR[PM_PAR_XPOS] = source->peak->xf;
+    PAR[PM_PAR_YPOS] = source->peak->yf;
+    return model;
+}
+
+// construct an initial PSF model for each object
+bool psphotGuessModels (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, psMaskType maskVal) {
+
+  psTimerStart ("psphot");
+
+  // setup the PSF fit radius details
+  psphotInitRadiusPSF (recipe, psf->type);
+
+  for (int i = 0; i < sources->n; i++) {
+    pmSource *source = sources->data[i];
+
+    // skip non-astronomical objects (very likely defects)
+    if (source->type == PM_SOURCE_TYPE_DEFECT) continue;
+    if (source->type == PM_SOURCE_TYPE_SATURATED) continue;
+
+    // XXX if a source is faint, it will not have moments measured.
+    // it must be modelled as a PSF.  In this case, we need to use
+    // the peak centroid to get the coordinates and get the peak flux
+    // from the image?
+    pmModel *modelEXT;
+    if (!source->moments) {
+        modelEXT = wildGuess(source, psf);
+    } else {
+        // use the source moments, etc to guess basic model parameters
+        modelEXT = pmSourceModelGuess (source, psf->type);
+        if (!modelEXT) {
+            modelEXT = wildGuess(source, psf);
+        }
+        // these valuse are set in pmSourceModelGuess, should this rule be in there as well?
+        if (source->mode &  PM_SOURCE_MODE_SATSTAR) {
+            modelEXT->params->data.F32[PM_PAR_XPOS] = source->moments->x;
+            modelEXT->params->data.F32[PM_PAR_YPOS] = source->moments->y;
+        } else {
+            modelEXT->params->data.F32[PM_PAR_XPOS] = source->peak->xf;
+            modelEXT->params->data.F32[PM_PAR_YPOS] = source->peak->yf;
+        }
+    }
+
+    // set PSF parameters for this model (apply 2D shape model)
+    pmModel *modelPSF = pmModelFromPSF (modelEXT, psf);
+    if (modelPSF == NULL) {
+        psError(PSPHOT_ERR_PSF, false,
+                "Failed to determine PSF model at r,c = (%d,%d); trying centre of image",
+                source->peak->y, source->peak->x);
+        //
+        // Try the centre of the image
+        //
+        modelEXT->params->data.F32[PM_PAR_XPOS] = 0.5*readout->image->numCols;
+        modelEXT->params->data.F32[PM_PAR_YPOS] = 0.5*readout->image->numRows;
+        modelPSF = pmModelFromPSF (modelEXT, psf);
+        if (modelPSF == NULL) {
+            psError(PSPHOT_ERR_PSF, false,
+                    "Failed to determine PSF model at centre of image");
+            psFree(modelEXT);
+            return false;
+        }
+
+        source->mode |= PM_SOURCE_MODE_BADPSF;
+    }
+    psFree (modelEXT);
+
+    // XXX need to define the guess flux?
+    // set the fit radius based on the object flux limit and the model
+    psphotCheckRadiusPSF (readout, source, modelPSF);
+
+    // set the source PSF model
+    source->modelPSF = modelPSF;
+    source->modelPSF->residuals = psf->residuals;
+
+    pmSourceCacheModel (source, maskVal);
+  }
+  psLogMsg ("psphot.models", 4, "built models for %ld objects: %f sec\n", sources->n, psTimerMark ("psphot"));
+  return true;
+}
+
+// XXX do we always know which model is supposed to be used?
Index: /branches/eam_branch_20071023/psphot/src/psphotImageLoop.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotImageLoop.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotImageLoop.c	(revision 15358)
@@ -0,0 +1,101 @@
+# include "psphotStandAlone.h"
+
+# define ESCAPE(MESSAGE) { \
+  psError(PSPHOT_ERR_DATA, false, MESSAGE); \
+  psFree (view); \
+  return false; \
+}
+
+bool psphotImageLoop (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);
+    
+    // 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.WEIGHT");
+	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.WEIGHT");
+	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", 4, "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", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+		if (! readout->data_exists) { continue; }
+
+		// run the actual photometry analysis on this chip/cell/readout
+		if (!psphotReadout (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;
+		}
+	    }
+	}
+
+	// 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.WEIGHT
+// 
Index: /branches/eam_branch_20071023/psphot/src/psphotImageMedian.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotImageMedian.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotImageMedian.c	(revision 15358)
@@ -0,0 +1,428 @@
+# include "psphotInternal.h"
+static int npass = 0;
+
+// we have 4 possibilities: (INTERNAL or I/O file) and (exists or not)
+// select model pixels (from output background model file, or create internal file)
+static pmReadout *get_model_readout(const char *name, // name of internal/external file
+                                    const pmConfig *config, // configuration information
+                                    const pmFPAview *view,
+                                    pmFPA *inFPA,
+                                    const psImageBinning *binning) {
+    pmReadout *model = NULL;
+
+    bool status = true;
+    pmFPAfile *file = psMetadataLookupPtr(&status, config->files, name);
+    if (file == NULL) {
+        // we are not using PSPHOT.BACKMDL as an I/O file: define an internal version
+        model = pmFPAfileDefineInternal (config->files, name, binning->nXruff, binning->nYruff, PS_TYPE_F32);
+    } else {
+        if (file->mode == PM_FPA_MODE_INTERNAL) {
+            // we are not using PSPHOT.BACKMDL as an I/O file: already defined above
+            model = file->readout;
+        } else {
+            // we are using PSPHOT.BACKMDL as an I/O file: select readout or create
+            model = pmFPAviewThisReadout (view, file->fpa);
+            if (model == NULL) {
+                // readout does not yet exist: create from input
+                // XXX we have an inconsistency in this calculation here and in pmFPACopy
+                // XXX use the psImageBinning functions to set the output image size
+                pmFPAfileCopyStructureView (file->fpa, inFPA, binning->nXbin, binning->nYbin, view);
+                model = pmFPAviewThisReadout (view, file->fpa);
+                PS_ASSERT (binning->nXruff == model->image->numCols, false);
+                PS_ASSERT (binning->nYruff == model->image->numRows, false);
+            }
+        }
+    }
+
+    return model;
+}
+
+// generate the median in NxN boxes, clipping heavily
+// linear interpolation to generate full-scale model
+bool psphotImageMedian (pmConfig *config, const pmFPAview *view, psMaskType maskVal)
+{
+    bool status = true;
+    static char *defaultStatsName = "FITTED_MEAN";
+
+    pmReadout *readout = NULL;
+    pmReadout *background = NULL;
+    pmReadout *backSub = NULL;
+
+    psTimerStart ("psphot");
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+
+    // user supplied seed, if available
+    unsigned long seed = psMetadataLookupS32 (&status, recipe, "IMSTATS_SEED");
+    if (!status) {
+        seed = 0;
+    }
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, seed);
+
+    // subtract this amount extra from the sky
+    float SKY_BIAS = psMetadataLookupF32 (&status, recipe, "SKY_BIAS");
+    if (!status) {
+        SKY_BIAS = 0;
+    }
+
+    // supply the sky background statistics options
+    char *statsName = psMetadataLookupStr (&status, recipe, "SKY_STAT");
+    if (statsName == NULL) {
+        statsName = defaultStatsName;
+    }
+    psStatsOptions statsOptionLocation = psStatsOptionFromString(statsName);
+    if (!(statsOptionLocation & (PS_STAT_SAMPLE_MEAN |
+                                 PS_STAT_SAMPLE_MEDIAN |
+                                 PS_STAT_ROBUST_MEDIAN |
+                                 PS_STAT_ROBUST_QUARTILE |
+                                 PS_STAT_CLIPPED_MEAN |
+                                 PS_STAT_FITTED_MEAN |
+                                 PS_STAT_FITTED_MEAN_V2 |
+                                 PS_STAT_FITTED_MEAN_V3 |
+                                 PS_STAT_FITTED_MEAN_V4))) {
+        statsOptionLocation = PS_STAT_FITTED_MEAN;
+    }
+
+    psStatsOptions statsOptionWidth = PS_STAT_NONE;
+    if (statsOptionLocation & (PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_MEDIAN)) {
+        statsOptionWidth = PS_STAT_SAMPLE_STDEV;
+    } else if (statsOptionLocation & (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_QUARTILE)) {
+#if 1
+        statsOptionWidth = PS_STAT_ROBUST_STDEV; // not set; => NaN
+#else
+        statsOptionWidth = PS_STAT_FITTED_STDEV;
+#endif
+    } else if (statsOptionLocation & PS_STAT_FITTED_MEAN) {
+        statsOptionWidth = PS_STAT_FITTED_STDEV;
+    } else if (statsOptionLocation & PS_STAT_CLIPPED_MEAN) {
+        statsOptionWidth = PS_STAT_CLIPPED_STDEV;
+    } else if (statsOptionLocation & PS_STAT_FITTED_MEAN_V2) {
+        statsOptionWidth = PS_STAT_FITTED_STDEV_V2;
+    } else if (statsOptionLocation & PS_STAT_FITTED_MEAN_V3) {
+        statsOptionWidth = PS_STAT_FITTED_STDEV_V3;
+    } else if (statsOptionLocation & PS_STAT_FITTED_MEAN_V4) {
+        statsOptionWidth = PS_STAT_FITTED_STDEV_V4;
+    } else {
+        psAbort("Unable to estimate variance of selected statsOptionLocations 0x%x", statsOptionLocation);
+    }
+
+    const psStatsOptions statsOption = statsOptionLocation | statsOptionWidth;
+    psStats *stats = psStatsAlloc (statsOption);
+
+    // set range for old-version of sky statistic
+    if (statsOptionLocation & PS_STAT_ROBUST_QUARTILE) {
+        stats->min = 0.25;
+        stats->max = 0.75;
+    }
+
+    // set user-option for number of pixels per region
+    stats->nSubsample = psMetadataLookupF32 (&status, recipe, "IMSTATS_NPIX");
+    if (!status) {
+        stats->nSubsample = 1000;
+    }
+
+    // optionally set the binsize
+    stats->binsize = psMetadataLookupF32 (&status, recipe, "SKY_HISTOGRAM_BINS");
+    if (status) {
+        stats->options |= PS_STAT_USE_BINSIZE;
+    }
+
+    // optionally set the sigma clipping
+    stats->clipSigma = psMetadataLookupF32 (&status, recipe, "SKY_CLIP_SIGMA");
+    if (!status) {
+        if ((stats->options & PS_STAT_FITTED_MEAN) || (stats->options & PS_STAT_FITTED_MEAN_V2)) {
+            stats->clipSigma = 1.0;
+        } else {
+            stats->clipSigma = 3.0;
+        }
+    }
+
+    // stats is not initialized by psStats???  use this to save the input options
+    // XXX re-work this to use the new psStatsInit function
+    psStats *statsDefaults = psStatsAlloc (statsOption);
+    *statsDefaults = *stats;
+
+    // find the currently selected readout
+    pmFPAfile *file = psMetadataLookupPtr (&status, config->files, "PSPHOT.INPUT");
+    pmFPA *inFPA = file->fpa;
+    readout = pmFPAviewThisReadout (view, inFPA);
+
+    psImage *image = readout->image;
+    psImage *mask  = readout->mask;
+
+    // I have the fine image size, I know the binning factor, determine the ruff image size
+    psImageBinning *binning = psImageBinningAlloc();
+    binning->nXfine = image->numCols;
+    binning->nYfine = image->numRows;
+    binning->nXbin  = psMetadataLookupS32 (&status, recipe, "BACKGROUND.XBIN");
+    binning->nYbin  = psMetadataLookupS32 (&status, recipe, "BACKGROUND.YBIN");
+
+    psImageBinningSetRuffSize(binning, PS_IMAGE_BINNING_CENTER);
+    psImageBinningSetSkip(binning, image);
+    status = psMetadataAddPtr(recipe, PS_LIST_TAIL, "PSPHOT.BACKGROUND.BINNING", PS_DATA_UNKNOWN | PS_META_REPLACE, "Background binning", binning);
+    PS_ASSERT (status, false);
+
+    // we save the binning structure for use in psphotMagnitudes
+    pmReadout *model = get_model_readout("PSPHOT.BACKMDL", config, view, inFPA, binning);
+    pmReadout *modelStdev = get_model_readout("PSPHOT.BACKMDL.STDEV", config, view, inFPA, binning);
+
+    psF32 **modelData = model->image->data.F32;
+    psF32 **modelStdevData = modelStdev->image->data.F32;
+
+    // measure clipped median for subimages
+    psRegion ruffRegion = {0,0,0,0};
+    psRegion fineRegion = {0,0,0,0};
+    for (int iy = 0; iy < model->image->numRows; iy++) {
+        for (int ix = 0; ix < model->image->numCols; ix++) {
+
+            // convert the ruff grid cell to the equivalent fine grid cell
+            // XXX we need to watch out for row0,col0
+            ruffRegion = psRegionSet (ix, ix + 2, iy, iy + 2);
+            fineRegion = psImageBinningSetFineRegion (binning, ruffRegion);
+            fineRegion = psRegionForImage (image, fineRegion);
+
+            psImage *subset  = psImageSubset (image, fineRegion);
+            if (!subset->numCols || !subset->numRows) {
+                psFree (subset);
+                continue;
+            }
+            psImage *submask = psImageSubset (mask, fineRegion);
+
+            // reset the default values
+            *stats = *statsDefaults;
+
+            // Use the selected background statistic for the first pass
+            // If it fails, fall back on the "ROBUST_MEDIAN" version
+            // If both fail, set the pixel to NAN and (below) interpolate
+            // XXX psImageBackground will probably be renamed psImageStats
+            // XXX don't bother trying if there are no valid pixels...
+
+	    psVector *sample = NULL;
+
+	    // turn on stats tracing in desired cells
+	    # if (0)
+	    psMetadata *plots = psMetadataLookupPtr (&status, recipe, "DIAGNOSTIC.PLOTS");
+	    assert (plots);
+	    
+	    int xPlot = psMetadataLookupS32 (&status, plots, "IMAGE.BACKGROUND.CELL.HISTOGRAM.X");
+	    assert (status);
+	    int yPlot = psMetadataLookupS32 (&status, plots, "IMAGE.BACKGROUND.CELL.HISTOGRAM.Y");
+	    assert (status);
+
+	    bool gotX = (xPlot < 0) || (xPlot == ix);
+	    bool gotY = (yPlot < 0) || (yPlot == iy);
+
+	    if (gotX && gotY) {
+		psTraceSetLevel ("psLib.math.vectorFittedStats_v4", 6);
+		psTraceSetLevel ("psLib.math.vectorRobustStats", 6);
+	    } else {
+		psTraceSetLevel ("psLib.math.vectorFittedStats_v4", 0);
+		psTraceSetLevel ("psLib.math.vectorRobustStats", 0);
+	    }	    
+	    # endif
+
+            if (psImageBackground(stats, &sample, subset, submask, maskVal, rng)) {
+                if (stats->options & PS_STAT_ROBUST_QUARTILE) {
+                    modelData[iy][ix] = stats->robustMedian;
+                } else {
+                    modelData[iy][ix] = psStatsGetValue(stats, statsOptionLocation);
+                }
+                modelStdevData[iy][ix] = psStatsGetValue(stats, statsOptionWidth);
+
+		// supply sample to plotting routing
+		psphotDiagnosticPlots (config, "IMAGE.BACKGROUND.CELL.HISTOGRAM", ix, iy, modelData[iy][ix], modelStdevData[iy][ix], sample);
+		psFree (sample);
+            } else {
+                psStatsOptions currentOptions = stats->options;
+                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);
+                    modelData[iy][ix] = modelStdevData[iy][ix] = NAN;
+                } else {
+                    modelData[iy][ix] = psStatsGetValue (stats, PS_STAT_ROBUST_MEDIAN);
+                    modelStdevData[iy][ix] = psStatsGetValue(stats, PS_STAT_ROBUST_STDEV);
+
+		    // supply sample to plotting routing
+		    psphotDiagnosticPlots (config, "IMAGE.BACKGROUND.CELL.HISTOGRAM", ix, iy, modelData[iy][ix], modelStdevData[iy][ix], sample);
+		    psFree (sample);
+                }
+                // drop errors caused by psImageBackground failures
+                // XXX we probably should trap and exit on serious failures
+                psErrorClear();
+                stats->options = currentOptions;
+            }
+            modelData[iy][ix] += SKY_BIAS;
+            psFree (subset);
+            psFree (submask);
+        }
+    }
+
+    if (psTraceGetLevel("psphot") > 5) {
+        char name[256];
+        sprintf (name, "backraw.%02d.fits", npass);
+        psphotSaveImage (NULL, model->image, name);
+    }
+
+    // patch over bad regions (use average of 8 possible neighbor pixels)
+    // XXX consider testing all pixels against the 8 neighbors and replacing outliers...
+    double Count = 0;                   // number of good pixels
+    double Value = 0;                   // sum of good pixel's value
+    double ValueStdev = 0;              // sum of good pixel's standard deviations
+    for (int iy = 0; iy < model->image->numRows; iy++) {
+        for (int ix = 0; ix < model->image->numCols; ix++) {
+            if (!isnan(modelData[iy][ix])) {
+                Value += modelData[iy][ix];
+                ValueStdev += modelStdevData[iy][ix];
+                Count++;
+                continue;
+            }
+
+            double value = 0;
+            double count = 0;
+            for (int jy = iy - 1; jy <= iy + 1; jy++) {
+                if (jy <   0) continue;
+                if (jy >= model->image->numRows) continue;
+                for (int jx = ix - 1; jx <= ix + 1; jx++) {
+                    if (!jx && !jy) continue;
+                    if (jx   <   0) continue;
+                    if (jx   >= model->image->numCols) continue;
+                    value += modelData[jy][jx];
+                    count += 1.0;
+                }
+            }
+            if (count > 0) modelData[iy][ix] = value / count;
+        }
+    }
+    if (Count == 0) {
+        psError (PSPHOT_ERR_DATA, true, "failed to build background image");
+	psFree(stats);
+	psFree(statsDefaults);
+	psFree(binning);
+	psFree(rng);
+        return false;
+    }
+
+    Value /= Count;
+    ValueStdev /= Count;
+
+    // patch over remaining bad regions (use global average)
+    for (int iy = 0; iy < model->image->numRows; iy++) {
+        for (int ix = 0; ix < model->image->numCols; ix++) {
+            if (!isnan(modelData[iy][ix])) continue;
+            modelData[iy][ix] = Value;
+            modelStdevData[iy][ix] = ValueStdev;
+        }
+    }
+
+    psLogMsg ("psphot", PS_LOG_MINUTIA, "build median image: %f sec\n", psTimerMark ("psphot"));
+
+    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);
+    psLogMsg ("psphot", PS_LOG_INFO, "image sky : mean %f stdev %f", Value, ValueStdev);
+
+    // measure image and background stats and save for later output
+    psStats *statsBck = psStatsAlloc (PS_STAT_SAMPLE_MEAN |
+                                      PS_STAT_SAMPLE_STDEV |
+                                      PS_STAT_MIN |
+                                      PS_STAT_MAX);
+    psImageStats (statsBck, model->image, 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->image->numCols);
+    psMetadataAddS32 (recipe, PS_LIST_TAIL, "MSKY_NY",
+                      PS_META_REPLACE, "sky model size (y)",      model->image->numRows);
+    psLogMsg ("psphot", PS_LOG_INFO, "background sky : min %f mean %f max %f stdev %f",
+              statsBck->min, statsBck->sampleMean, statsBck->max, statsBck->sampleStdev);
+    psFree (statsBck);
+
+    // select background pixels, from output background file, or create
+    file = psMetadataLookupPtr (&status, config->files, "PSPHOT.BACKGND");
+    if (file) {
+        // we are using PSPHOT.BACKGND as an I/O file: select readout or create
+        if (file->mode == PM_FPA_MODE_INTERNAL) {
+            background = file->readout;
+        } else {
+            background = pmFPAviewThisReadout (view, file->fpa);
+        }
+        if (background == NULL) {
+            // readout does not yet exist: create from input
+            pmFPAfileCopyStructureView (file->fpa, inFPA, 1, 1, view);
+            background = pmFPAviewThisReadout (view, file->fpa);
+            if ((image->numCols != background->image->numCols) || (image->numRows != background->image->numRows)) {
+                psError (PSPHOT_ERR_PROG, true, "inconsistent sizes for background dimensions");
+                return false;
+            }
+        }
+    } else {
+        background = pmFPAfileDefineInternal (config->files, "PSPHOT.BACKGND", image->numCols, image->numRows, PS_TYPE_F32);
+    }
+    psF32 **backData = background->image->data.F32;
+
+    // linear interpolation to full-scale
+    if (!psImageUnbin (background->image, model->image, binning)) {
+        psError (PSPHOT_ERR_PROG, true, "inconsistent sizes for unbinning");
+        return false;
+    }
+
+    psLogMsg ("psphot", PS_LOG_MINUTIA, "build resampled image: %f sec\n", psTimerMark ("psphot"));
+
+    // back-sub image pixels, from output background file (don't create if not requested)
+    file = psMetadataLookupPtr (&status, config->files, "PSPHOT.BACKSUB");
+    if (file) {
+        // we are using PSPHOT.BACKSUB as an I/O file: select readout or create
+        backSub = pmFPAviewThisReadout (view, file->fpa);
+        if (backSub == NULL) {
+            // readout does not yet exist: create from input
+            pmFPAfileCopyStructureView (file->fpa, inFPA, 1, 1, view);
+            backSub = pmFPAviewThisReadout (view, file->fpa);
+        }
+    }
+
+    if (psTraceGetLevel("psphot") > 5) {
+        char name[256];
+        sprintf (name, "image.%02d.fits", npass);
+        psphotSaveImage (NULL, image, name);
+        sprintf (name, "back.%02d.fits", npass);
+        psphotSaveImage (NULL, background->image, name);
+        sprintf (name, "mask.%02d.fits", npass);
+        psphotSaveImage (NULL, mask, name);
+        sprintf (name, "backmdl.%02d.fits", npass);
+        psphotSaveImage (NULL, model->image, name);
+    }
+
+    // subtract the background model (save in backSub, if requested)
+    for (int j = 0; j < image->numRows; j++) {
+        for (int i = 0; i < image->numCols; i++) {
+            image->data.F32[j][i] -= backData[j][i];
+            if (backSub) {
+                backSub->image->data.F32[j][i] = image->data.F32[j][i];
+            }
+        }
+    }
+
+    if (psTraceGetLevel("psphot") > 5) {
+        char name[256];
+        sprintf (name, "backsub.%02d.fits", npass);
+        psphotSaveImage (NULL, image, name);
+    }
+    npass ++;
+
+    psLogMsg ("psphot", PS_LOG_INFO, "subtracted background model: %f sec\n", psTimerMark ("psphot"));
+    psFree(stats);
+    psFree(statsDefaults);
+    psFree(binning);
+    psFree(rng);
+
+    // the pmReadout selected in this function are all view on entries in config->files
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotInternal.h
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotInternal.h	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotInternal.h	(revision 15358)
@@ -0,0 +1,17 @@
+/* this file is included by the internal, library functions. */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#ifndef PSPHOT_INTERNAL_H
+#define PSPHOT_INTERNAL_H
+
+#include <stdio.h>
+#include <strings.h>  // for strcasecmp
+#include <unistd.h>   // for unlink
+#include <pslib.h>
+#include <psmodules.h>
+#include "psphot.h"
+
+#endif
Index: /branches/eam_branch_20071023/psphot/src/psphotIsophotal.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotIsophotal.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotIsophotal.c	(revision 15358)
@@ -0,0 +1,8 @@
+# include "psphot.h"
+
+bool psphotIsophotal (pmSource *source, psMetadata *recipe, psMaskType maskVal) {
+
+  psLogMsg ("psphot", PS_LOG_INFO, "not implemented\n");
+  return true;
+
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotKernelFromPSF.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotKernelFromPSF.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotKernelFromPSF.c	(revision 15358)
@@ -0,0 +1,20 @@
+# include "psphot.h"
+
+psKernel *psphotKernelFromPSF (pmSource *source, int nPix) {
+
+  assert (source);
+  assert (source->psfFlux); // XXX build if needed?
+
+  int x0 = source->peak->xf - source->psfFlux->col0;
+  int y0 = source->peak->yf - source->psfFlux->row0;
+
+  // need to decide on the size: dynamically? statically?
+  psKernel *psf = psKernelAlloc (-nPix, +nPix, -nPix, +nPix);
+
+  for (int j = psf->yMin; j <= psf->yMax; j++) {
+    for (int i = psf->xMin; i <= psf->xMax; i++) {
+      psf->kernel[j][i] = source->psfFlux->data.F32[y0 + j][x0 + i];
+    }
+  }
+  return psf;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotKron.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotKron.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotKron.c	(revision 15358)
@@ -0,0 +1,8 @@
+# include "psphot.h"
+
+bool psphotKron (pmSource *source, psMetadata *recipe, psMaskType maskVal) {
+
+  psLogMsg ("psphot", PS_LOG_INFO, "not implemented\n");
+  return true;
+
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotLoadPSF.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotLoadPSF.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotLoadPSF.c	(revision 15358)
@@ -0,0 +1,24 @@
+# include "psphotInternal.h"
+
+// load an externally supplied psf model
+pmPSF *psphotLoadPSF (pmConfig *config, const pmFPAview *view, psMetadata *recipe) {
+
+    // find the currently selected readout
+    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.PSF.LOAD");
+    if (!readout) return NULL;
+
+    // check if a PSF model is supplied by the user
+    pmPSF *psf = psMetadataLookupPtr (NULL, readout->parent->parent->analysis, "PSPHOT.PSF");
+    if (psf == NULL) {
+        psLogMsg ("psphot", 3, "no psf supplied for this readout");
+        return NULL;
+    }
+
+    if (!psphotPSFstats (readout, recipe, 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;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotMagnitudes.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotMagnitudes.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotMagnitudes.c	(revision 15358)
@@ -0,0 +1,50 @@
+# include "psphotInternal.h"
+
+bool psphotMagnitudes(psArray *sources,
+                      psMetadata *recipe,
+                      pmPSF *psf,
+                      pmReadout *background,
+                      psMaskType maskVal,
+                      psMaskType mark)
+{
+    bool status = false;
+    int Nap = 0;
+
+    psTimerStart ("psphot");
+
+    pmSourceMagnitudesInit (recipe);
+
+    // XXX require that we have a background model, or
+    // allow it to be missing, setting local sky to 0.0?
+    PS_ASSERT (background, false);
+
+    // the binning details are saved on the analysis metadata
+    psImageBinning *binning = psMetadataLookupPtr(&status, recipe, "PSPHOT.BACKGROUND.BINNING");
+    PS_ASSERT (status, false);
+
+    bool IGNORE_GROWTH = psMetadataLookupBool (&status, recipe, "IGNORE_GROWTH");
+    bool INTERPOLATE_AP = 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;
+
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+        status = pmSourceMagnitudes (source, psf, photMode, maskVal, mark);
+        if (status) Nap ++;
+
+        source->sky = psImageUnbinPixel(source->peak->x, source->peak->y, background->image, binning);
+        if (isnan(source->sky) && false) {
+          psError(PSPHOT_ERR_SKY, false, "Setting pmSource.sky");
+          psErrorStackPrint(NULL, " ");
+          psErrorClear();
+        }
+    }
+
+    psLogMsg ("psphot.magnitudes", PS_LOG_DETAIL, "measure magnitudes : %f sec for %ld objects (%d with apertures)\n", psTimerMark ("psphot"), sources->n, Nap);
+    return true;
+}
+
+// XXX add in a measurement of the bright and faint completeness values
+// XXX push these into the recipe
Index: /branches/eam_branch_20071023/psphot/src/psphotMakeFluxScale.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotMakeFluxScale.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotMakeFluxScale.c	(revision 15358)
@@ -0,0 +1,63 @@
+# include "psphotInternal.h"
+
+bool psphotMakeFluxScale (psImage *image, psMetadata *recipe, pmPSF *psf) {
+
+    bool status = false;
+
+    psTimerStart ("residuals");
+
+    int Nx = psMetadataLookupS32(&status, recipe, "PSF.FLUXSCALE.NX");
+    int Ny = psMetadataLookupS32(&status, recipe, "PSF.FLUXSCALE.NY");
+
+    // stats structure for use by ApTrend : XXX make parameters user setable
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+
+    pmTrend2D *trend = pmTrend2DAlloc (PM_TREND_MAP, image, Nx, Ny, stats);
+    
+    psVector *xPts = psVectorAlloc (Nx*Ny, PS_TYPE_F32);
+    psVector *yPts = psVectorAlloc (Nx*Ny, PS_TYPE_F32);
+    psVector *fPts = psVectorAlloc (Nx*Ny, PS_TYPE_F32);
+
+    int Npts = 0;
+
+    // generate a set of test normalized PSF fluxes filling the grid 
+    for (int ix = 0; ix < Nx; ix++) {
+	for (int iy = 0; iy < Ny; iy++) {
+	    
+	    float x = psImageBinningGetFineX (trend->map->binning, ix + 0.5);
+	    float y = psImageBinningGetFineY (trend->map->binning, iy + 0.5);
+
+	    // create normalized model object at xc,yc 
+	    pmModel *model = pmModelFromPSFforXY (psf, x, y, 1.0);
+
+	    // measure the fitMag for this model
+	    float fitSum = model->modelFlux (model->params);
+	    assert (fitSum > 0);
+	    assert (isfinite(fitSum));
+
+	    xPts->data.F32[Npts] = x;
+	    yPts->data.F32[Npts] = y;
+	    fPts->data.F32[Npts] = fitSum;
+	    Npts ++;
+	    assert (Npts <= xPts->nalloc);
+	    psFree (model);
+	}
+    }
+    xPts->n = Npts;
+    yPts->n = Npts;
+    fPts->n = Npts;
+    
+    // XXX test for errors here
+    pmTrend2DFit (trend, NULL, 0xff, xPts, yPts, fPts, NULL);
+    
+    // XXX do something useful to measure residual statistics
+
+    psf->FluxScale = trend;
+
+    psFree (xPts);
+    psFree (yPts);
+    psFree (fPts);
+    psFree (stats);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotMakeResiduals.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotMakeResiduals.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotMakeResiduals.c	(revision 15358)
@@ -0,0 +1,262 @@
+# include "psphotInternal.h"
+
+bool psphotMakeResiduals (psArray *sources, psMetadata *recipe, pmPSF *psf, psMaskType maskVal) {
+
+    bool status, isPSF;
+    double flux, dflux;
+    psU8 mflux;
+
+    psTimerStart ("residuals");
+
+    if (!psMetadataLookupBool(&status, recipe, "PSF.RESIDUALS")) return true;
+
+    int SPATIAL_ORDER = psMetadataLookupS32(&status, recipe, "PSF.RESIDUALS.SPATIAL_ORDER");
+    PS_ASSERT (status, false);
+    if (SPATIAL_ORDER != 0 && SPATIAL_ORDER != 1) {
+        psError(PSPHOT_ERR_CONFIG, true, "PSF.RESIDUALS.SPATIAL_ORDER must be 0 or 1 (not %d)",
+                SPATIAL_ORDER);
+        return false;
+    }
+
+    int xBin = psMetadataLookupS32(&status, recipe, "PSF.RESIDUALS.XBIN");
+    PS_ASSERT (status, false);
+    PS_ASSERT (xBin != 0, false);
+
+    int yBin = psMetadataLookupS32(&status, recipe, "PSF.RESIDUALS.YBIN");
+    PS_ASSERT (status, false);
+    PS_ASSERT (yBin != 0, false);
+
+    float nSigma = psMetadataLookupF32(&status, recipe, "PSF.RESIDUALS.NSIGMA");
+    PS_ASSERT (status, false);
+
+    char *modeString = psMetadataLookupStr(&status, recipe, "PSF.RESIDUALS.INTERPOLATION");
+    PS_ASSERT (status, false);
+
+    psImageInterpolateMode mode = psImageInterpolateModeFromString (modeString);
+    if (mode == PS_INTERPOLATE_NONE) {
+        psError(PSPHOT_ERR_CONFIG, false, "invalid interpolation in psphot.config");
+        return false;
+    }
+
+    char *statString = psMetadataLookupStr(&status, recipe, "PSF.RESIDUALS.STATISTIC");
+    PS_ASSERT (status, false);
+
+    psStatsOptions statOption = psStatsOptionFromString (statString);
+    if (!statOption) {
+        psError(PSPHOT_ERR_CONFIG, false, "invalid residual statistic in psphot.config");
+        return false;
+    }
+
+    // user parameters:
+    // size of aperture (determine from source images?)
+    // binning factor
+
+    // select the subset of sources which are the PSFSTARs
+    // for each input source:
+    // - construct a residual image, renormalized
+    // - construct a renormalized weight image
+    // - construct a new mask image
+
+    // construct the output residual table (Nx*DX,Ny*DY)
+    // for each output pixel:
+    // - construct a histogram of the values & weights (interpolate to the common pixel coordinate)
+    // - measure the robust median & sigma
+    // - reject (mask) input pixels which are outliers
+    // - re-measure the robust median & sigma
+    // - set output pixel, weight, and mask
+
+    const int badMask = 1;              // mask bits
+    const int poorMask = 2;             //       from psImageInterpolate
+    const int clippedMask = 4;          // mask bit set for clipped values
+
+    bool offImage = false;              // pixel is off the image
+
+    // determine the maximum image size from the input sources
+    int xSize = 0;
+    int ySize = 0;
+
+    psVector *xC = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *yC = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    // build (DATA - MODEL) [an image] for each psf star
+    psArray *input = psArrayAllocEmpty (100);
+    for (int i = 0; i < sources->n; i++) {
+
+        pmSource *source = sources->data[i];
+
+        if (!(source->mode & PM_SOURCE_MODE_PSFSTAR)) continue;
+
+        // which model to use?
+        pmModel *model = pmSourceGetModel (&isPSF, source);
+        if (model == NULL) continue;  // model must be defined
+
+        psImage *image  = psImageCopy (NULL, source->pixels,   PS_TYPE_F32);
+        psImage *weight = psImageCopy (NULL, source->weight,   PS_TYPE_F32);
+        psImage *mask   = psImageCopy (NULL, source->maskView, PS_TYPE_U8);
+        pmModelSub (image, mask, model, PM_MODEL_OP_FUNC, maskVal);
+
+        // re-normalize image and weight
+        float Io = model->params->data.F32[PM_PAR_I0];
+        psBinaryOp (image, image, "/", psScalarAlloc(Io, PS_TYPE_F32));
+        psBinaryOp (weight, weight, "/", psScalarAlloc(Io*Io, PS_TYPE_F32));
+
+        // we will interpolate the image and weight - include the mask or not?
+        // XXX consider better values for the mask bits
+        psImageInterpolateOptions *interp =
+            psImageInterpolateOptionsAlloc(mode, image, weight, NULL, 0xff, 0.0, 0.0, badMask, poorMask, 0.0);
+        psArrayAdd (input,  100, interp);
+
+        // save the X,Y position for future reference
+        xC->data.F32[xC->n] = model->params->data.F32[PM_PAR_XPOS];
+        yC->data.F32[yC->n] = model->params->data.F32[PM_PAR_YPOS];
+        psVectorExtend (xC, 100, 1);
+        psVectorExtend (yC, 100, 1);
+
+        xSize = PS_MAX (xSize, image->numCols);
+        ySize = PS_MAX (ySize, image->numRows);
+
+        // free up the excess references
+        psFree (mask);
+        psFree (image);
+        psFree (weight);
+        psFree (interp);
+    }
+    pmResiduals *resid = pmResidualsAlloc (xSize, ySize, xBin, yBin);
+
+    // x(resid) = (x(image) - Xo)*xBin + xCenter
+
+    psVector *fluxes  = psVectorAlloc (input->n, PS_TYPE_F32);
+    psVector *dfluxes = psVectorAlloc (input->n, PS_TYPE_F32);
+    psVector *fmasks  = psVectorAlloc (input->n, PS_TYPE_U8);
+
+    // statistic to use to determine baseline for clipping
+    psStats *fluxClip     = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    psStats *fluxClipDef  = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    // statistic to use to determine output flux
+    // XXX make API to convert statOption for MEAN/MEDIAN in to corresponding STDEV?
+    psStats *fluxStats    = psStatsAlloc (statOption | PS_STAT_SAMPLE_STDEV);
+    psStats *fluxStatsDef = psStatsAlloc (statOption | PS_STAT_SAMPLE_STDEV);
+
+    // Use psF64 to minimize overflow problems?
+    psImage *A = psImageAlloc(3, 3, PS_TYPE_F64); // Least-squares matrix
+    psVector *B = psVectorAlloc(3, PS_TYPE_F64); // Least-squares vector
+
+    // Solve MODEL = R + x R_x + y R_y in pixel-by-pixel a least-squares sense
+    // (If SPATIAL_ORDER == 0, just solve MODEL = R)
+    for (int oy = 0; resid != NULL && oy < resid->Ro->numRows; oy++) {
+        for (int ox = 0; ox < resid->Ro->numCols; ox++) {
+
+            // build the vector of data values for this output pixel
+            for (int i = 0; i < input->n; i++) {
+
+                psImageInterpolateOptions *interp = input->data[i];
+
+                // fractional image position
+                float ix = (ox + 0.5 - resid->xCenter) / (float) xBin + xC->data.F32[i] - interp->image->col0;
+                float iy = (oy + 0.5 - resid->yCenter) / (float) yBin + yC->data.F32[i] - interp->image->row0;
+
+                mflux = 0;
+                offImage = false;
+                if (psImageInterpolate (&flux, &dflux, &mflux, ix, iy, interp) == PS_INTERPOLATE_STATUS_OFF) {
+                    // This pixel is off the image
+                    offImage = true;
+                }
+                fluxes->data.F32[i] = flux;
+                dfluxes->data.F32[i] = dflux;
+                fmasks->data.U8[i] = mflux;
+                // fprintf (stderr, "%f %f : %f %f (%d)\n", ix, iy, flux, dflux, fmasks->data.U8[i]);
+            }
+
+            // measure the robust median to determine a baseline reference value
+            *fluxClip = *fluxClipDef;
+            psVectorStats (fluxClip, fluxes, NULL, fmasks, 0xff);
+            psErrorClear();             // clear (ignore) any outstanding errors
+
+            // mark input pixels which are more than N sigma from the median
+            for (int i = 0; i < fluxes->n; i++) {
+                float delta = fluxes->data.F32[i] - fluxClip->robustMedian;
+                float sigma = sqrt (dfluxes->data.F32[i]);
+                float swing = fabs(delta) / sigma;
+
+                // make this a user option
+                if (swing > nSigma) {
+                    fmasks->data.U8[i] = clippedMask;
+                }
+            }
+
+            if (offImage || SPATIAL_ORDER == 0) {
+                // measure the desired statistic on the unclipped pixels
+                *fluxStats = *fluxStatsDef;
+                psVectorStats (fluxStats, fluxes, NULL, fmasks, 0xff);
+                psErrorClear();         // clear (ignore) any outstanding errors
+
+                resid->Ro->data.F32[oy][ox] = psStatsGetValue(fluxStats, statOption);
+                resid->Rx->data.F32[oy][ox] = resid->Ry->data.F32[oy][ox] = 0.0;
+                //resid->weight->data.F32[oy][ox] = fluxStats->sampleStdev;
+            } else {
+                assert (SPATIAL_ORDER == 1);
+                psImageInit(A, 0.0);
+                psVectorInit(B, 0.0);
+                for (int i = 0; i < fluxes->n; i++) {
+                    if (fmasks->data.U8[i]) continue;
+                    B->data.F64[0] += fluxes->data.F32[i]/dfluxes->data.F32[i];
+                    B->data.F64[1] += fluxes->data.F32[i]*xC->data.F32[i]/dfluxes->data.F32[i];
+                    B->data.F64[2] += fluxes->data.F32[i]*yC->data.F32[i]/dfluxes->data.F32[i];
+
+                    A->data.F64[0][0] += 1.0/dfluxes->data.F32[i];
+                    A->data.F64[1][0] += xC->data.F32[i]/dfluxes->data.F32[i];
+                    A->data.F64[2][0] += yC->data.F32[i]/dfluxes->data.F32[i];
+
+                    A->data.F64[1][1] += PS_SQR(xC->data.F32[i])/dfluxes->data.F32[i];
+                    A->data.F64[2][2] += PS_SQR(yC->data.F32[i])/dfluxes->data.F32[i];
+                    A->data.F64[1][2] += xC->data.F32[i]*yC->data.F32[i]/dfluxes->data.F32[i];
+                }
+
+                A->data.F64[0][1] = A->data.F64[1][0];
+                A->data.F64[0][2] = A->data.F64[2][0];
+                A->data.F64[2][1] = A->data.F64[1][2];
+
+                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->Ro->data.F32[oy][ox] = B->data.F64[0];
+                resid->Rx->data.F32[oy][ox] = B->data.F64[1];
+                resid->Ry->data.F32[oy][ox] = B->data.F64[2];
+                //resid->weight->data.F32[oy][ox] = XXX;
+            }
+        }
+    }
+
+    psFree (A);
+    psFree (B);
+
+    psLogMsg ("psphot.pspsf", PS_LOG_INFO, "generate residuals for %ld objects: %f sec\n", input->n, psTimerMark ("residuals"));
+
+    psFree (xC);
+    psFree (yC);
+    psFree (input);
+
+    psFree (fluxes);
+    psFree (dfluxes);
+    psFree (fmasks);
+
+    psFree (fluxStats);
+    psFree (fluxStatsDef);
+    psFree (fluxClip);
+    psFree (fluxClipDef);
+
+    if (resid != NULL && psTraceGetLevel("psphot") > 5) {
+      psphotSaveImage (NULL, resid->Ro,     "resid.ro.fits");
+      psphotSaveImage (NULL, resid->Rx,     "resid.rx.fits");
+      psphotSaveImage (NULL, resid->Ry,     "resid.ry.fits");
+      psphotSaveImage (NULL, resid->weight, "resid.wt.fits");
+      psphotSaveImage (NULL, resid->mask,   "resid.mk.fits");
+    }
+
+    psf->residuals = resid;
+    return (resid != NULL) ? true : false;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotMaskReadout.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotMaskReadout.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotMaskReadout.c	(revision 15358)
@@ -0,0 +1,24 @@
+# include "psphotInternal.h"
+
+bool psphotMaskReadout (pmReadout *readout, psMetadata *recipe, psMaskType maskVal) {
+
+    bool status;
+
+    // mask the excluded outer pixels
+    // these coordinates refer to the parent image
+    // these bounds will saturate on the subimage
+    // negative upper bounds will subtract from the *subimage*
+    float XMIN  = psMetadataLookupF32 (&status, recipe, "XMIN");
+    float XMAX  = psMetadataLookupF32 (&status, recipe, "XMAX");
+    float YMIN  = psMetadataLookupF32 (&status, recipe, "YMIN");
+    float YMAX  = psMetadataLookupF32 (&status, recipe, "YMAX");
+    psRegion valid = psRegionSet (XMIN, XMAX, YMIN, YMAX);
+
+    // restrict the supplied region above to the valid area on the image
+    psRegion keep = psRegionForImage (readout->image, valid);
+
+    // psImageKeepRegion assumes the region refers to the parent coordinates
+    psImageKeepRegion (readout->mask, keep, "OR", maskVal);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotMergeSources.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotMergeSources.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotMergeSources.c	(revision 15358)
@@ -0,0 +1,52 @@
+# include "psphotInternal.h"
+
+// add newly selected sources to the existing list of sources
+bool psphotMergeSources (psArray *oldSources, psArray *newSources) {
+
+    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) {
+        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);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotModelGroupInit.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotModelGroupInit.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotModelGroupInit.c	(revision 15358)
@@ -0,0 +1,25 @@
+# include "psphotInternal.h"
+
+// Add locally-defined models here.  As these mature, they can be moved to 
+// psModule/src/objects/models
+
+# include "models/pmModel_TEST1.c"
+# include "models/pmModel_STRAIL.c"
+
+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},
+};
+
+void psphotModelClassInit (void) 
+{ 
+
+    // if pmModelClassInit returns false, we have already init'ed
+    if (!pmModelClassInit ()) return;
+
+    int Nmodels = sizeof (userModels) / sizeof (pmModelClass);
+    for (int i = 0; i < Nmodels; i++) {
+	pmModelClassAdd (&userModels[i]);
+    }
+    return;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotModelTest.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotModelTest.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotModelTest.c	(revision 15358)
@@ -0,0 +1,235 @@
+# include "psphotInternal.h"
+# define PM_SOURCE_FIT_PSF_X_EXT PM_SOURCE_FIT_PSF_AND_SKY
+
+// XXX add more test information?
+bool psphotModelTest (pmConfig *config, const pmFPAview *view, psMetadata *recipe, psMaskType maskVal, psMaskType mark) {
+
+    bool status;
+    int modelType = -1;
+    float obsMag, fitMag, value;
+    char name[64];
+    pmPSF *psf = NULL;
+    pmSourceFitMode fitMode;
+
+    // run model fitting tests on a single source?
+    if (!psMetadataLookupBool (&status, recipe, "TEST_FIT")) return false;
+
+    psTimerStart ("modelTest");
+
+    // find the currently selected readout
+    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
+    PS_ASSERT_PTR_NON_NULL (readout, false);
+
+    // use poissonian errors or local-sky errors
+    bool POISSON_ERRORS = psMetadataLookupBool (&status, recipe, "POISSON_ERRORS");
+    if (!status) POISSON_ERRORS = true;
+    pmSourceFitModelInit (15, 0.1, 1.0, POISSON_ERRORS);
+
+    // find the various fitting parameters (try test values first)
+    float INNER = psMetadataLookupF32 (&status, recipe, "TEST_FIT_INNER_RADIUS");
+    if (!status || !isfinite(INNER)) {
+        INNER = psMetadataLookupF32 (&status, recipe, "SKY_INNER_RADIUS");
+    }
+    float OUTER = psMetadataLookupF32 (&status, recipe, "TEST_FIT_OUTER_RADIUS");
+    if (!status || !isfinite(OUTER)) {
+        OUTER = psMetadataLookupF32 (&status, recipe, "SKY_OUTER_RADIUS");
+    }
+    float RADIUS = psMetadataLookupF32 (&status, recipe, "TEST_FIT_RADIUS");
+    if (!status || !isfinite(RADIUS)) {
+        RADIUS = psMetadataLookupF32 (&status, recipe, "PSF_FIT_RADIUS");
+    }
+    float mRADIUS = psMetadataLookupF32 (&status, recipe, "TEST_MOMENTS_RADIUS");
+    if (!status || !isfinite(mRADIUS)) {
+        mRADIUS = psMetadataLookupF32 (&status, recipe, "PSF_MOMENTS_RADIUS");
+    }
+
+    // define the source of interest
+    float xObj     = psMetadataLookupF32 (&status, recipe, "TEST_FIT_X");
+    float yObj     = psMetadataLookupF32 (&status, recipe, "TEST_FIT_Y");
+    if (!isfinite(xObj) || !isfinite(yObj)) psAbort ("object position is not defined");
+
+    // what fitting mode to use?
+    fitMode = PM_SOURCE_FIT_EXT;
+    char *fitModeWord = psMetadataLookupStr (&status, recipe, "TEST_FIT_MODE");
+    if (fitModeWord && !strcasecmp (fitModeWord, "PSF")) fitMode = PM_SOURCE_FIT_PSF;
+    if (fitModeWord && !strcasecmp (fitModeWord, "CONV")) fitMode = PM_SOURCE_FIT_PSF_X_EXT;
+    if (fitModeWord && !strcasecmp (fitModeWord, "DEFAULT")) fitMode = PM_SOURCE_FIT_EXT;
+
+    // construct the source structures
+    pmSource *source = pmSourceAlloc();
+    source->peak = pmPeakAlloc (xObj, yObj, 0, 0);
+    pmSourceDefinePixels (source, readout, xObj, yObj, OUTER);
+
+    // in fitMode, psf sets the model type
+    if (fitMode == PM_SOURCE_FIT_PSF) {
+        psf = psphotLoadPSF (config, view, recipe);
+        if (!psf) psAbort("PSF_INPUT_FILE not supplied");
+        modelType = psf->type;
+        source->type = PM_SOURCE_TYPE_STAR;
+    }
+    if (fitMode == PM_SOURCE_FIT_EXT) {
+        // find the model: supplied by user or first in the PSF_MODEL list
+        char *modelName  = psMetadataLookupStr (&status, recipe, "TEST_FIT_MODEL");
+        if (!status || !strcasecmp (modelName, "DEFAULT")) {
+            // get the list pointers for the PSF_MODEL entries
+
+            psList *list = NULL;
+            psMetadataItem *mdi = psMetadataLookup (recipe, "PSF_MODEL");
+            if (mdi == NULL) psAbort("missing PSF_MODEL selection");
+            if (mdi->type == PS_DATA_STRING) {
+                list = psListAlloc(NULL);
+                psListAdd (list, PS_LIST_HEAD, mdi);
+            } else {
+                if (mdi->type != PS_DATA_METADATA_MULTI) psAbort("missing PSF_MODEL selection");
+                list = psMemIncrRefCounter(mdi->data.list);
+            }
+
+            // take the first list element
+            psMetadataItem *item = psListGet (list, PS_LIST_HEAD);
+            modelName = item->data.V;
+        }
+        modelType = pmModelClassGetType (modelName);
+        if (modelType < 0) psAbort("unknown model %s", modelName);
+        source->type = PM_SOURCE_TYPE_EXTENDED;
+    }
+    if (fitMode == PM_SOURCE_FIT_PSF_X_EXT) {
+        // we need to load BOTH a psf and an ext model
+        psf = psphotLoadPSF (config, view, recipe);
+        if (!psf) psAbort("PSF_INPUT_FILE not supplied");
+
+        // find the model: supplied by user or first in the PSF_MODEL list
+        char *modelName  = psMetadataLookupStr (&status, recipe, "TEST_FIT_MODEL");
+        if (!status || !strcasecmp (modelName, "DEFAULT")) {
+            // get the list pointers for the PSF_MODEL entries
+
+            psList *list = NULL;
+            psMetadataItem *mdi = psMetadataLookup (recipe, "PSF_MODEL");
+            if (mdi == NULL) psAbort("missing PSF_MODEL selection");
+            if (mdi->type == PS_DATA_STRING) {
+                list = psListAlloc(NULL);
+                psListAdd (list, PS_LIST_HEAD, mdi);
+            } else {
+                if (mdi->type != PS_DATA_METADATA_MULTI) psAbort("missing PSF_MODEL selection");
+                list = psMemIncrRefCounter(mdi->data.list);
+            }
+
+            // take the first list element
+            psMetadataItem *item = psListGet (list, PS_LIST_HEAD);
+            modelName = item->data.V;
+        }
+        modelType = pmModelClassGetType (modelName);
+        if (modelType < 0) psAbort("unknown model %s", modelName);
+        source->type = PM_SOURCE_TYPE_EXTENDED;
+    }
+
+    // find the local sky
+    status = pmSourceLocalSky (source, PS_STAT_SAMPLE_MEDIAN, INNER, maskVal, mark);
+    if (!status) psAbort("pmSourceLocalSky error");
+
+    // get the source moments
+    status = pmSourceMoments (source, mRADIUS);
+    if (!status) psAbort("psSourceMoments error");
+    source->peak->value = source->moments->Peak;
+
+    fprintf (stderr, "sum: %f @ (%f, %f)\n", source->moments->Sum, source->moments->x, source->moments->y);
+    fprintf (stderr, "moments: %f, %f - %f\n", source->moments->Sx, source->moments->Sy, source->moments->Sxy);
+
+    psEllipseMoments moments;
+    moments.x2 = source->moments->Sx;
+    moments.y2 = source->moments->Sy;
+    moments.xy = source->moments->Sxy;
+    psEllipseAxes axes = psEllipseMomentsToAxes (moments, 20.0);
+
+    fprintf (stderr, "axes: %f @ (%f, %f)\n", axes.theta*180/M_PI, axes.major, axes.minor);
+
+    // get the initial model parameter guess
+    pmModel *model = pmSourceModelGuess (source, modelType);
+    source->modelEXT = model;
+
+    // if any parameters are defined by the user, take those values
+    int nParams = pmModelClassParameterCount (modelType);
+    psF32 *params = model->params->data.F32;
+    params[PM_PAR_XPOS] = xObj; // XXX use the user-supplied value,
+    params[PM_PAR_YPOS] = yObj; // XXX or use the centroid
+    for (int i = 0; i < nParams; i++) {
+        if (i == PM_PAR_XPOS) continue;
+        if (i == PM_PAR_YPOS) continue;
+
+        sprintf (name, "TEST_FIT_PAR%d", i);
+        value = psMetadataLookupF32 (&status, recipe, name);
+        if (status && isfinite (value)) {
+            params[i] = value;
+        }
+    }
+
+    float area = params[4]*params[5];
+    fprintf (stderr, "peak: %f @ (%f, %f)\n", source->moments->Sum*area, (double)source->peak->x, (double)source->peak->y);
+
+    // for PSF fitting, set the shape parameters based on the PSF & source position
+    if (fitMode == PM_SOURCE_FIT_PSF) {
+        source->modelPSF = pmModelFromPSF (model, psf);
+        psFree (model);
+        model = source->modelPSF;
+        params = model->params->data.F32;
+    }
+
+    // list model input shape
+    psEllipseShape shape;
+    shape.sx  = 1.4 / model->params->data.F32[4];
+    shape.sy  = 1.4 / model->params->data.F32[5];
+    shape.sxy = model->params->data.F32[6];
+    axes = psEllipseShapeToAxes (shape, 20.0);
+
+    fprintf (stderr, "guess: %f @ (%f, %f)\n", axes.theta*180/M_PI, axes.major, axes.minor);
+
+    fprintf (stderr, "input parameters: \n");
+    for (int i = 0; i < nParams; i++) {
+        fprintf (stderr, "%d : %f\n", i, params[i]);
+    }
+
+    // define the pixels used for the fit
+    psImageKeepCircle (source->maskObj, xObj, yObj, RADIUS, "OR", mark);
+    psphotSaveImage (NULL, source->maskObj, "mask1.fits");
+
+    char *fitset = psMetadataLookupStr (&status, recipe, "TEST_FIT_SET");
+    if (status) {
+        status = psphotFitSet (source, model, fitset, fitMode, maskVal);
+        exit (0);
+    }
+
+    if (fitMode == PM_SOURCE_FIT_PSF_X_EXT) {
+        // build the psf for the object
+        source->modelPSF = pmModelFromPSF (model, psf);
+        source->modelEXT = model;
+        status = psphotPSFConvModel (source, recipe, maskVal);
+        model = source->modelConv;
+        params = model->params->data.F32;
+    } else {
+        status = pmSourceFitModel (source, model, fitMode, maskVal);
+    }
+
+    // measure the source mags
+    pmSourcePhotometryModel (&fitMag, model);
+    pmSourcePhotometryAper  (&obsMag, model, source->pixels, source->maskObj, maskVal);
+    fprintf (stderr, "ap: %f, fit: %f, apmifit: %f, nIter: %d\n", obsMag, fitMag, obsMag - fitMag, model->nIter);
+
+    // write out positive object
+    psphotSaveImage (NULL, source->pixels, "object.fits");
+
+    // subtract object, leave local sky
+    // pmModelSub (source->pixels, source->maskObj, model, PM_MODEL_OP_FULL, maskVal);
+    pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
+
+    fprintf (stderr, "output parameters: \n");
+    for (int i = 0; i < nParams; i++) {
+        fprintf (stderr, "%d : %f\n", i, params[i]);
+    }
+
+    // write out
+    psphotSaveImage (NULL, source->pixels, "resid.fits");
+    psphotSaveImage (NULL, source->maskObj, "mask.fits");
+
+    psLogMsg ("psphot", PS_LOG_INFO, "model test : %f sec\n", psTimerMark ("modelTest"));
+
+    exit (0);
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotModelWithPSF.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotModelWithPSF.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotModelWithPSF.c	(revision 15358)
@@ -0,0 +1,404 @@
+# include "psphot.h"
+
+bool psphotModelWithPSF_LMM (
+    psMinimization *min,
+    psImage *covar,
+    psVector *params,
+    psMinConstraint *constraint,
+    pmSource *source,
+    const psKernel *psf,
+    psMinimizeLMChi2Func func)
+{
+    psTrace("psphot", 3, "---- begin ----\n");
+    PS_ASSERT_PTR_NON_NULL(min, false);
+    PS_ASSERT_VECTOR_NON_NULL(params, false);
+    PS_ASSERT_VECTOR_NON_EMPTY(params, false);
+    PS_ASSERT_VECTOR_TYPE(params, PS_TYPE_F32, false);
+    psVector *paramMask = NULL;
+    if (constraint != NULL) {
+        paramMask = constraint->paramMask;
+        if (paramMask != NULL) {
+            PS_ASSERT_VECTOR_TYPE(paramMask, PS_TYPE_U8, false);
+            PS_ASSERT_VECTORS_SIZE_EQUAL(params, paramMask, false);
+        }
+    }
+    PS_ASSERT_PTR_NON_NULL(func, false);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+
+    psMinimizeLMLimitFunc checkLimits = NULL;
+    if (constraint) {
+        checkLimits = constraint->checkLimits;
+    }
+
+    // this function has test values and current values for several things
+    // the current value is in lower case
+    // the test value is in upper case
+
+    // allocate internal arrays (current vs Guess)
+    psImage *alpha   = psImageAlloc(params->n, params->n, PS_TYPE_F32);
+    psImage *Alpha   = psImageAlloc(params->n, params->n, PS_TYPE_F32);
+    psVector *beta   = psVectorAlloc(params->n, PS_TYPE_F32);
+    psVector *Beta   = psVectorAlloc(params->n, PS_TYPE_F32);
+    psVector *Params = psVectorAlloc(params->n, PS_TYPE_F32);
+
+    psF32 Chisq = 0.0;
+    psF32 lambda = 0.001;
+    psF32 dLinear = 0.0;
+
+    // generate PCM data storage structure
+    pmPCMData *pcm = pmPCMDataAlloc (params, paramMask, source);
+
+    // calculate initial alpha and beta, set chisq (min->value)
+    min->value = psphotModelWithPSF_SetABX(alpha, beta, params, paramMask, pcm, source, psf, func);
+    if (isnan(min->value)) {
+        min->iter = min->maxIter;
+        return(false);
+    }
+    // dump some useful info if trace is defined
+    if (psTraceGetLevel("psphot") >= 6) {
+        p_psImagePrint(psTraceGetDestination(), alpha, "alpha guess (0)");
+        p_psVectorPrint(psTraceGetDestination(), beta, "beta guess (0)");
+    }
+    if (psTraceGetLevel("psphot") >= 5) {
+        p_psVectorPrint(psTraceGetDestination(), params, "params guess (0)");
+    }
+
+    // iterate until the tolerance is reached, or give up
+    while ((min->iter < min->maxIter) && ((min->lastDelta > min->tol) || !isfinite(min->lastDelta))) {
+        psTrace("psphot", 5, "Iteration number %d.  (max iterations is %d).\n", min->iter, min->maxIter);
+        psTrace("psphot", 5, "Last delta is %f.  Min->tol is %f.\n", min->lastDelta, min->tol);
+
+
+        // set a new guess for Alpha, Beta, Params
+        if (!psMinLM_GuessABP(Alpha, Beta, Params, alpha, beta, params, paramMask, checkLimits, lambda, &dLinear)) {
+            min->iter ++;
+            lambda *= 10.0;
+            continue;
+        }
+
+        // dump some useful info if trace is defined
+        if (psTraceGetLevel("psphot") >= 6) {
+            p_psImagePrint(psTraceGetDestination(), Alpha, "alpha guess (1)");
+            p_psVectorPrint(psTraceGetDestination(), Beta, "beta guess (1)");
+        }
+        if (psTraceGetLevel("psphot") >= 5) {
+            p_psVectorPrint(psTraceGetDestination(), Params, "params guess (1)");
+        }
+
+        // calculate Chisq for new guess, update Alpha & Beta
+        Chisq = psphotModelWithPSF_SetABX(Alpha, Beta, Params, paramMask, pcm, source, psf, func);
+        if (isnan(Chisq)) {
+            min->iter ++;
+            lambda *= 10.0;
+            continue;
+        }
+
+        // convergence criterion:
+        // compare the delta (min->value - Chisq) with the
+        // expected delta from the linear model (dLinear)
+        // accept new guess if it is an improvement (rho > 0), or else increase lambda
+        psF32 rho = (min->value - Chisq) / dLinear;
+
+        psTrace("psphot", 5, "last chisq: %f, new chisq %f, delta: %f, rho: %f\n", min->value,
+                Chisq, min->lastDelta, rho);
+
+        // dump some useful info if trace is defined
+        if (psTraceGetLevel("psphot") >= 6) {
+            p_psImagePrint(psTraceGetDestination(), Alpha, "alpha guess (2)");
+            p_psVectorPrint(psTraceGetDestination(), Beta, "beta guess (2)");
+        }
+
+        /* if (Chisq < min->value) {  */
+        if (rho > 0.0) {
+            min->lastDelta = (min->value - Chisq) / (source->pixels->numCols*source->pixels->numRows - params->n);
+            min->value = Chisq;
+            alpha  = psImageCopy(alpha, Alpha, PS_TYPE_F32);
+            beta   = psVectorCopy(beta, Beta, PS_TYPE_F32);
+            params = psVectorCopy(params, Params, PS_TYPE_F32);
+            lambda *= 0.1;
+
+	    // save the new convolved model image
+	    psFree (source->modelFlux);
+	    source->modelFlux = pmPCMDataSaveImage(pcm);
+        } else {
+            lambda *= 10.0;
+        }
+        min->iter++;
+    }
+    psTrace("psphot", 5, "chisq: %f, last delta: %f, Niter: %d\n", min->value, min->lastDelta, min->iter);
+
+    // construct & return the covariance matrix (if requested)
+    if (covar != NULL) {
+        if (!psMinLM_GuessABP(covar, Beta, Params, alpha, beta, params, paramMask, NULL, 0.0, NULL)) {
+            psTrace ("psphot", 5, "failure to calculate covariance matrix\n");
+        }
+    }
+
+    // free the internal temporary data
+    psFree(alpha);
+    psFree(Alpha);
+    psFree(beta);
+    psFree(Beta);
+    psFree(Params);
+    psFree(pcm);
+
+    if (min->iter == min->maxIter) {
+        psTrace("psphot", 3, "---- end (false) ----\n");
+        return(false);
+    }
+
+    psTrace("psphot", 3, "---- end (true) ----\n");
+    return(true);
+}
+
+psF32 psphotModelWithPSF_SetABX(
+    psImage  *alpha,
+    psVector *beta,
+    const psVector *params,
+    const psVector *paramMask,
+    pmPCMData *pcm,
+    const pmSource *source,
+    const psKernel *psf,
+    psMinimizeLMChi2Func func)
+{
+    // XXX: Check vector sizes.
+    PS_ASSERT_IMAGE_NON_NULL(alpha, NAN);
+    PS_ASSERT_VECTOR_NON_NULL(beta, NAN);
+    PS_ASSERT_VECTOR_NON_NULL(params, NAN);
+
+    PS_ASSERT_PTR_NON_NULL(source, NAN);
+    PS_ASSERT_IMAGE_NON_NULL(source->pixels, NAN);
+    PS_ASSERT_IMAGE_NON_NULL(source->weight, NAN);
+    PS_ASSERT_IMAGE_NON_NULL(source->maskObj, NAN);
+
+    PS_ASSERT_VECTOR_TYPE(params, PS_TYPE_F32, false);
+    if (paramMask) {
+        PS_ASSERT_VECTOR_TYPE(paramMask, PS_TYPE_MASK, false);
+    }
+
+    // generate the model and derivative images for this parameter set
+
+    // storage for model derivatives
+    psVector *deriv = psVectorAlloc(params->n, PS_TYPE_F32);
+
+    // working vector to store local coordinate
+    psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+
+    psImageInit (pcm->model, 0.0);
+    for (int n = 0; n < params->n; n++) {
+	if (!pcm->dmodels->data[n]) continue;
+	psImageInit (pcm->dmodels->data[n], 0.0);
+    }
+
+    // fill in the coordinate and value entries
+    for (psS32 i = 0; i < source->pixels->numRows; i++) {
+        for (psS32 j = 0; j < source->pixels->numCols; j++) {
+
+	    // XXX can we skip some of the data points where the model
+	    // is not going to be fitted??
+
+            // skip masked points
+	    // XXX probably should not skipped masked points: 
+	    // XXX skip if convolution of unmasked pixels will not see this pixel
+            // if (source->maskObj->data.U8[i][j]) {
+	    // continue;
+	    // }
+
+            // skip zero-weight points
+	    // XXX why is this not masked?
+            // if (source->weight->data.F32[i][j] == 0) {
+	    // continue;
+	    // }
+            // skip nan value points
+	    // XXX why is this not masked?
+            // if (!isfinite(source->pixels->data.F32[i][j])) {
+	    // continue;
+	    // }
+
+            // Convert i/j to image space:
+            coord->data.F32[0] = (psF32) (j + source->pixels->col0);
+            coord->data.F32[1] = (psF32) (i + source->pixels->row0);
+
+	    pcm->model->data.F32[i][j] = func (deriv, params, coord);
+
+	    for (int n = 0; n < params->n; n++) {
+	      if ((paramMask != NULL) && (paramMask->data.U8[n])) { continue; }
+	      psImage *dmodel = pcm->dmodels->data[n];
+	      dmodel->data.F32[i][j] = deriv->data.F32[n];
+	    }
+        }
+    }
+    psFree(coord);
+    psFree(deriv);
+
+
+    // convolve model and dmodel arrays with PSF
+    psImageConvolveDirect (pcm->modelConv, pcm->model, psf);
+    for (int n = 0; n < pcm->dmodels->n; n++) {
+      if (pcm->dmodels->data[n] == NULL) continue;
+      psImage *dmodel = pcm->dmodels->data[n];
+      psImage *dmodelConv = pcm->dmodelsConv->data[n];
+      psImageConvolveDirect (dmodelConv, dmodel, psf);
+    }
+
+    // XXX TEST : SAVE IMAGES
+# if (1) 
+    psphotSaveImage (NULL, psf->image, "psf.fits");
+    psphotSaveImage (NULL, pcm->model, "model.fits");
+    psphotSaveImage (NULL, pcm->modelConv, "modelConv.fits");
+    psphotSaveImage (NULL, source->pixels, "obj.fits");
+    psphotSaveImage (NULL, source->maskObj, "mask.fits");
+    psphotSaveImage (NULL, source->weight, "weight.fits");
+# endif
+
+    // zero alpha and beta for summing below
+    psImageInit (alpha, 0.0);
+    psVectorInit (beta, 0.0);
+    float chisq = 0.0;
+
+    for (psS32 i = 0; i < source->pixels->numRows; i++) {
+        for (psS32 j = 0; j < source->pixels->numCols; j++) {
+	  // XXX are we doing the right thing with the mask?
+            // skip masked points
+            if (source->maskObj->data.U8[i][j]) {
+                continue;
+            }
+            // skip zero-weight points
+            if (source->weight->data.F32[i][j] == 0) {
+                continue;
+            }
+            // skip nan value points
+            if (!isfinite(source->pixels->data.F32[i][j])) {
+                continue;
+            }
+
+	    float ymodel  = pcm->modelConv->data.F32[i][j];
+	    float yweight = 1.0 / source->weight->data.F32[i][j];
+	    float delta = ymodel - source->pixels->data.F32[i][j];
+
+	    chisq += PS_SQR(delta) * yweight;
+
+	    if (isnan(delta))
+	      psAbort("nan in delta");
+	    if (isnan(chisq))
+	      psAbort("nan in chisq");
+
+	    for (psS32 n1 = 0; n1 < params->n; n1++) {
+	      if ((paramMask != NULL) && (paramMask->data.U8[n1])) {
+                continue;
+	      }
+	      psImage *dmodel = pcm->dmodelsConv->data[n1];
+	      float weight = dmodel->data.F32[i][j] * yweight;
+	      for (psS32 n2 = 0; n2 <= n1; n2++) {
+                if ((paramMask != NULL) && (paramMask->data.U8[n2])) {
+		  continue;
+                }
+		dmodel = pcm->dmodelsConv->data[n2];
+                alpha->data.F32[n1][n2] += weight * dmodel->data.F32[i][j];
+	      }
+	      beta->data.F32[n1] += weight * delta;
+	    }
+	}
+    }
+
+    // calculate lower-left half of alpha
+    for (psS32 j = 1; j < params->n; j++) {
+        for (psS32 k = 0; k < j; k++) {
+            alpha->data.F32[k][j] = alpha->data.F32[j][k];
+        }
+    }
+
+    // fill in pivots if we apply a mask
+    if (paramMask != NULL) {
+        for (psS32 j = 0; j < params->n; j++) {
+            if (paramMask->data.U8[j]) {
+                alpha->data.F32[j][j] = 1;
+                beta->data.F32[j] = 1;
+            }
+        }
+    }
+
+    return(chisq);
+}
+
+static void pmPCMDataFree (pmPCMData *pcm) {
+
+    if (pcm == NULL) return;
+
+    psFree (pcm->model);
+    psFree (pcm->modelConv);
+    psFree (pcm->dmodels);
+    psFree (pcm->dmodelsConv);
+    return;
+}
+
+pmPCMData *pmPCMDataAlloc (
+    const psVector *params,
+    const psVector *paramMask,
+    pmSource *source) {
+
+    pmPCMData *pcm = (pmPCMData *) psAlloc(sizeof(pmPCMData));
+    psMemSetDeallocator(pcm, (psFreeFunc) pmPCMDataFree);
+
+    // Allocate storage images for raw model and derivative images
+    pcm->model = psImageCopy (NULL, source->pixels, PS_TYPE_F32);
+    pcm->dmodels = psArrayAlloc (params->n);
+    for (psS32 n = 0; n < params->n; n++) {
+      if ((paramMask != NULL) && (paramMask->data.U8[n])) { continue; }
+      pcm->dmodels->data[n] = psImageCopy (NULL, source->pixels, PS_TYPE_F32);
+    }
+
+    // Allocate storage images for convolved model and derivative images
+    pcm->modelConv = psImageCopy (NULL, source->pixels, PS_TYPE_F32);
+    pcm->dmodelsConv = psArrayAlloc (params->n);
+    for (psS32 n = 0; n < params->n; n++) {
+      if ((paramMask != NULL) && (paramMask->data.U8[n])) { continue; }
+      pcm->dmodelsConv->data[n] = psImageCopy (NULL, source->pixels, PS_TYPE_F32);
+    }
+
+    return pcm;
+}
+
+psImage *pmPCMDataSaveImage (pmPCMData *pcm) {
+
+    psImage *model = psImageCopy (NULL, pcm->modelConv, PS_TYPE_F32);
+   
+    return model;
+}
+
+/*
+ *
+ * we have a function func(param; value)
+ 
+ * basic LMM:
+ 
+ - fill in the data (x, y)
+ 
+ chisq = SetABX (alpha, beta, params, paramMask, x, y, dy, func)
+ 
+ while () {
+   GuessABP (Alpha, Beta, Params, alpha, beta, params, paramMask, checkLimits, lambda)
+   dLinear = dLinear(Beta, beta, lambda);
+   chisq = SetABX (alpha, beta, params, paramMask, x, y, dy, func)
+   convergence tests...
+ }
+ 
+ 
+
+ ** GuessABP:
+
+      f_c = sum_i (kern_i * func (x_i; p_o))
+
+df_c/dp_o = d/dp_o [sum_i (kern_i * func (x_i; p_o))]
+
+df_c/dp_o = sum_i (d/dp_o [kern_i * func (x_i; p_o)])
+
+df_c/dp_o = sum_i (kern_i * d/dp_o [func (x_i; p_o)])
+
+- generate image arrays for func, dfunc/dp_j (not masked)
+- convolve each with psf
+- measure delta = f_conv - data
+- etc
+*/
+
+
Index: /branches/eam_branch_20071023/psphot/src/psphotMosaicChip.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotMosaicChip.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotMosaicChip.c	(revision 15358)
@@ -0,0 +1,37 @@
+# include "psphotInternal.h"
+
+bool psphotMosaicChip(pmConfig *config, const pmFPAview *view, char *outFile, char *inFile)
+{
+    bool status;                        // Status of MD lookup
+
+    pmFPAfile *in = psMetadataLookupPtr(&status, config->files, inFile); // Input file
+    if (!status) {
+        psErrorStackPrint(stderr, "Can't find required I/O file!\n");
+        exit(EXIT_FAILURE);
+    }
+
+    pmFPAfile *out = psMetadataLookupPtr(&status, config->files, outFile); // Output file
+    if (!status) {
+        psErrorStackPrint(stderr, "Can't find required I/O file!\n");
+        exit(EXIT_FAILURE);
+    }
+
+    // XXXX we are failing to get the output hdu right
+    pmChip *outChip = pmFPAviewThisChip(view, out->fpa);
+    pmChip *inChip = pmFPAviewThisChip(view, in->fpa);
+    if (!outChip->hdu && !outChip->parent->hdu) {
+        const char *name = psMetadataLookupStr(&status, in->fpa->concepts, "FPA.NAME"); // Name of FPA
+        pmFPAAddSourceFromView(out->fpa, name, view, out->format);
+    }
+
+    // mosaic the chip, forcing a deep copy (resulting images are not subimages)
+    psTrace("pmChipMosaic", 5, "mosaic chip %s to %s (xbin,ybin: %d,%d to %d,%d)\n",
+            in->name, out->name, in->xBin, in->yBin, out->xBin, out->yBin);
+    status = pmChipMosaic(outChip, inChip, true, pmConfigMask("BLANK", config));
+    return status;
+}
+
+// XXX does this do everything needed?
+// * mask & weight
+// * loaded PSF model (in readout->analysis)
+// * loaded SRC sources (in readout->analysis)
Index: /branches/eam_branch_20071023/psphot/src/psphotMosaicSubimage.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotMosaicSubimage.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotMosaicSubimage.c	(revision 15358)
@@ -0,0 +1,46 @@
+# include "psphotInternal.h"
+
+// insert the source image into the outimage at Xo, Yo
+bool psphotMosaicSubimage (psImage *outImage, pmSource *source, int Xo, int Yo, int DX, int DY) {
+
+    psRegion inRegion, outRegion;
+    psImage *inImage = source->pixels;
+
+    // identify the region in the output image
+    outRegion = psRegionSet (Xo, Xo + DX, Yo, Yo + DX);
+    outRegion = psRegionForImage (outImage, outRegion);
+    int DXo = outRegion.x1 - outRegion.x0;
+    int DYo = outRegion.y1 - outRegion.y0;
+    if (DXo <= 0) return false;
+    if (DYo <= 0) return false;
+    
+    // center the input source in the output box
+    int dX = (DXo - 1) / 2;
+    int dY = (DYo - 1) / 2;
+
+    // int xo = inImage->col0 + inImage->numCols / 2;
+    // int yo = inImage->row0 + inImage->numRows / 2;
+
+    int xo = source->peak->xf;
+    int yo = source->peak->yf;
+
+    // adjust region to overlay input image pixels
+    inRegion = psRegionSet (xo - dX, xo + dX + 1, yo - dY, yo + dY + 1);
+    inRegion = psRegionForImage (inImage, inRegion);
+
+    float peak = source->peak->flux;
+
+    psImage *subImage = psImageSubset (inImage, inRegion);
+    psImage *newImage = psImageAlloc (subImage->numCols, subImage->numRows, PS_TYPE_F32);
+    for (int iy = 0; iy < newImage->numRows; iy++) {
+	for (int ix = 0; ix < newImage->numCols; ix++) {
+	    newImage->data.F32[iy][ix] = subImage->data.F32[iy][ix] / peak;
+	}
+    }
+
+    psImageOverlaySection (outImage, newImage, Xo, Yo, "=");
+
+    psFree (subImage);
+    psFree (newImage);
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotOutput.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotOutput.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotOutput.c	(revision 15358)
@@ -0,0 +1,155 @@
+# include "psphotInternal.h"
+
+pmReadout *psphotSelectBackground (pmConfig *config,
+                                   const pmFPAview *view,
+                                   const bool stdev // return background's standard deviation?
+                                   ) {
+
+    bool status;
+    pmReadout *background;
+
+    pmFPAfile *file = psMetadataLookupPtr (&status, config->files, "PSPHOT.BACKMDL");
+    if (!file) return NULL;
+    if (file->mode == PM_FPA_MODE_INTERNAL) {
+        background = file->readout;
+    } else {
+        background = pmFPAviewThisReadout (view, file->fpa);
+    }
+    return background;
+}
+
+bool psphotDumpConfig (pmConfig *config) {
+
+  psMetadataConfigWrite (config->site, "site.md");
+  psMetadataConfigWrite (config->camera, "camera.md");
+  psMetadataConfigWrite (config->recipes, "recipes.md");
+  psMetadataConfigWrite (config->arguments, "arguments.md");
+  psMetadataConfigWrite (config->files, "files.md");
+  return true;
+}
+
+int psphotSaveImage (psMetadata *header, psImage *image, char *filename) {
+
+    psFits *fits = psFitsOpen (filename, "w");
+    psFitsWriteImage (fits, NULL, image, 0, NULL);
+    psFitsClose (fits);
+    return (TRUE);
+}
+
+bool psphotDumpMoments (psMetadata *recipe, psArray *sources) {
+
+    bool status;
+
+    // optional dump of all rough source data
+    char *output = psMetadataLookupStr (&status, recipe, "MOMENTS_OUTPUT_FILE");
+    if (!status) return false;
+    if (output == NULL) return false;
+    if (output[0] == 0) return false;
+
+    pmMomentsWriteText (sources, output);
+    return true;
+}
+
+bool psphotDumpSource (pmSource *source, char *name) {
+
+    FILE *f = fopen (name, "w");
+    if (f == NULL) psAbort("can't open file");
+
+    for (int i = 0; i < source->pixels->numRows; i++) {
+        for (int j = 0; j < source->pixels->numCols; j++) {
+            // skip masked points
+            if (source->maskObj->data.U8[i][j]) {
+                continue;
+            }
+            // skip zero-weight points
+            if (source->weight->data.F32[i][j] == 0) {
+                continue;
+            }
+
+            fprintf (f, "%d %d %f %f %d\n",
+                     (j + source->pixels->col0),
+                     (i + source->pixels->row0),
+                     source->pixels->data.F32[i][j],
+                     1.0 / source->weight->data.F32[i][j],
+                     source->maskObj->data.U8[i][j]);
+        }
+    }
+    fclose (f);
+    return true;
+}
+
+bool psphotAddPhotcode (psMetadata *recipe, pmConfig *config, const pmFPAview *view) {
+
+    bool status;
+
+    pmFPAfile *input = psMetadataLookupPtr (&status, config->files, "PSPHOT.INPUT");
+    PS_ASSERT (status, false);
+
+    // determine PHOTCODE from fpa & view, overwrite in recipe
+    char *photcode = pmConceptsPhotcodeForView (config, input, view);
+    PS_ASSERT (photcode, false);
+
+    psMetadataAddStr (recipe, PS_LIST_TAIL, "PHOTCODE", PS_META_REPLACE, "photcode from FPA concepts", photcode);
+    psLogMsg ("psphot", 3, "PHOTCODE is %s", photcode);
+
+    psFree (photcode);
+    return true;
+}
+
+bool psphotSetHeaderNstars (psMetadata *recipe, psArray *sources) {
+
+    int nSrc = 0;
+
+    // count the number of sources which will be written
+    for (int i = 0; (sources != NULL) && (i < sources->n); i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+        pmModel *model = pmSourceGetModel (NULL, source);
+        if (model == NULL)
+            continue;
+        nSrc ++;
+    }
+    psMetadataAdd (recipe, PS_LIST_TAIL, "NSTARS", PS_DATA_S32 | PS_META_REPLACE, "NUMBER OF STARS", nSrc);
+    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) {
+
+    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");
+
+    // PSF model parameters (shape values for image center)
+    psMetadataItemSupplement (header, recipe, "NPSFSTAR");
+    psMetadataItemSupplement (header, recipe, "APLOSS");
+    psMetadataItemSupplement (header, recipe, "FWHM_X");
+    psMetadataItemSupplement (header, recipe, "FWHM_Y");
+    psMetadataItemSupplement (header, recipe, "ANGLE");
+
+    // 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");
+
+    // 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");
+
+    // XXX : don't require any of these about values to exist
+    psErrorClear ();
+
+    return header;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotPSFConvModel.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotPSFConvModel.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotPSFConvModel.c	(revision 15358)
@@ -0,0 +1,131 @@
+# include "psphot.h"
+
+// save as static values so they may be set externally
+static psF32 PM_SOURCE_FIT_MODEL_NUM_ITERATIONS = 15;
+static psF32 PM_SOURCE_FIT_MODEL_TOLERANCE = 0.1;
+// static psF32 PM_SOURCE_FIT_MODEL_WEIGHT = 1.0;
+// static bool  PM_SOURCE_FIT_MODEL_PIX_WEIGHTS = true;
+
+// input source has both modelPSF and modelEXT.  on successful exit, we set the
+// modelConv to contain the fitted parameters, and the modelFlux to contain the 
+// convolved model image.
+bool psphotPSFConvModel (pmSource *source, psMetadata *recipe, psMaskType maskVal) {
+    
+    bool status;
+
+    int psfSize = psMetadataLookupS32 (&status, recipe, "PCM_BOX_SIZE");
+    if (!status) {
+	psfSize = 2;
+    }
+
+    // make sure we save a cached copy of the psf flux
+    pmSourceCachePSF (source, maskVal);
+
+    // convert the cached cached psf model for this source to a psKernel
+    // XXX for the moment, hard-wire the kernel to be 5x5 (2 pix radius)
+    // XXX for the moment, hard-wire the kernel to be 9x9 (4 pix radius)
+    psKernel *psf = psphotKernelFromPSF (source, psfSize);
+
+    // psf must be normalized (integral = 1.0)
+    double sum = 0.0;
+    for (int i = 0; i < psf->image->numRows; i++) {
+	for (int j = 0; j < psf->image->numCols; j++) {
+	    sum += psf->image->data.F32[i][j];
+	}
+    }
+    assert (sum > 0.0);
+    for (int i = 0; i < psf->image->numRows; i++) {
+	for (int j = 0; j < psf->image->numCols; j++) {
+	    psf->image->data.F32[i][j] /= sum;
+	}
+    }
+
+# if (0)
+    // XXX sanity check: convolve with delta function should behave like unconvolved version
+    for (int i = 0; i < psf->image->numRows; i++) {
+	for (int j = 0; j < psf->image->numCols; j++) {
+	    psf->image->data.F32[i][j] = 0.0;
+	}
+    }
+    psf->image->data.F32[2][2] = 1.0;
+# endif
+
+    // generate copy of the model
+    // XXX we could modify the parameter values or even the model 
+    // here based on the observed seeing (some lookup table...)
+    pmModel *modelConv = pmModelCopy (source->modelEXT);
+    psVector *params  = modelConv->params;
+    psVector *dparams = modelConv->dparams;
+
+    // create the minimization constraints
+    psMinConstraint *constraint = psMinConstraintAlloc();
+    constraint->paramMask = psVectorAlloc (params->n, PS_TYPE_U8);
+    constraint->checkLimits = modelConv->modelLimits;
+
+    // set parameter mask based on fitting mode
+    // we fit a model without a floating sky term
+    int nParams = params->n - 1;
+    psVectorInit (constraint->paramMask, 0);
+    constraint->paramMask->data.U8[PM_PAR_SKY] = 1;
+
+    // force the floating parameters to fall within the contraint ranges
+    for (int i = 0; i < params->n; i++) {
+	modelConv->modelLimits (PS_MINIMIZE_PARAM_MIN, i, params->data.F32, NULL);
+	modelConv->modelLimits (PS_MINIMIZE_PARAM_MAX, i, params->data.F32, NULL);
+    }
+
+    // set up the minimization process
+    psMinimization *myMin = psMinimizationAlloc (PM_SOURCE_FIT_MODEL_NUM_ITERATIONS, PM_SOURCE_FIT_MODEL_TOLERANCE);
+
+    psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F32);
+
+    bool fitStatus = psphotModelWithPSF_LMM (myMin, covar, params, constraint, source, psf, modelConv->modelFunc);
+    for (int i = 0; i < dparams->n; i++) {
+        if (psTraceGetLevel("psphot") >= 4) {
+            fprintf (stderr, "%f ", params->data.F32[i]);
+        }
+        if ((constraint->paramMask != NULL) && constraint->paramMask->data.U8[i])
+            continue;
+        dparams->data.F32[i] = sqrt(covar->data.F32[i][i]);
+    }
+    psTrace ("psphot", 4, "niter: %d, chisq: %f", myMin->iter, myMin->value);
+
+    // renormalize output model image
+    float Io = params->data.F32[PM_PAR_I0];
+    for (int iy = 0; iy < source->modelFlux->numRows; iy++) {
+	for (int ix = 0; ix < source->modelFlux->numCols; ix++) {
+	    source->modelFlux->data.F32[iy][ix] /= Io;
+	}
+    }
+
+    // save the resulting chisq, nDOF, nIter
+    modelConv->chisq = myMin->value;
+    modelConv->nIter = myMin->iter;
+
+    // XXX I actually need to count the number of unmasked pixels here
+    modelConv->nDOF  = source->pixels->numCols*source->pixels->numRows  -  nParams;
+
+    modelConv->flags |= PM_MODEL_STATUS_FITTED;
+    if (!fitStatus) modelConv->flags |= PM_MODEL_STATUS_NONCONVERGE;
+
+    // models can go insane: reject these
+    bool onPic = true;
+    onPic &= (params->data.F32[PM_PAR_XPOS] >= source->pixels->col0);
+    onPic &= (params->data.F32[PM_PAR_XPOS] <  source->pixels->col0 + source->pixels->numCols);
+    onPic &= (params->data.F32[PM_PAR_YPOS] >= source->pixels->row0);
+    onPic &= (params->data.F32[PM_PAR_YPOS] <  source->pixels->row0 + source->pixels->numRows);
+    if (!onPic) {
+        modelConv->flags |= PM_MODEL_STATUS_OFFIMAGE;
+    }
+
+    source->mode |= PM_SOURCE_MODE_FITTED;
+    source->modelConv = modelConv;
+
+    psFree(myMin);
+    psFree(covar);
+    psFree(constraint);
+
+    bool retval = (onPic && fitStatus);
+    psTrace("psphot", 5, "---- %s(%d) end ----\n", __func__, retval);
+    return(retval);
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotPSFResiduals.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotPSFResiduals.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotPSFResiduals.c	(revision 15358)
@@ -0,0 +1,38 @@
+# include "psphotInternal.h"
+
+// construct an initial PSF model for each object 
+bool psphotResidPSF (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf) {
+
+  psTimerStart ("psphot");
+
+  for (int i = 0; i < sources->n; i++) {
+    pmSource *source = sources->data[i];
+
+    // only use the psf stars to build the residual model
+    if (!(source->mode & PM_SOURCE_MODE_PSFSTAR)) continue;
+
+    if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+	Xo = source->moments->x;
+	Yo = source->moments->y;
+	Io = source->peak->flux;
+    } else {
+	Xo = source->peak->xf;
+	Yo = source->peak->yf;
+	Io = source->peak->flux;
+    }
+
+    // set PSF parameters for this model (apply 2D shape model)
+    pmModel *modelPSF = pmModelFromPSFforXY (psf, Xo, Yo, Io);
+
+    // XXX need to define the guess flux?
+    // set the fit radius based on the object flux limit and the model
+    psphotCheckRadiusPSF (readout, source, modelPSF);
+
+    // set the source PSF model
+    source->modelPSF = modelPSF;
+  }
+  psLogMsg ("psphot.models", 4, "built models for %ld objects: %f sec\n", sources->n, psTimerMark ("psphot"));
+  return true;
+}
+
+// XXX do we always know which model is supposed to be used?  
Index: /branches/eam_branch_20071023/psphot/src/psphotParseCamera.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotParseCamera.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotParseCamera.c	(revision 15358)
@@ -0,0 +1,63 @@
+# include "psphotStandAlone.h"
+
+// define the needed / desired I/O files
+bool psphotParseCamera (pmConfig *config) {
+
+    bool status = false;
+
+    // the file to be loaded may have subdivisions at the cell and readout level
+    // we load into pmFPAfile *load, then reformat into pmFPAfile *input
+    pmFPAfile *load = pmFPAfileDefineFromArgs (&status, config, "PSPHOT.LOAD", "INPUT");
+    if (!status) {
+	psError(PSPHOT_ERR_CONFIG, false, "Failed to build FPA from PSPHOT.LOAD");
+	return status;
+    }
+    load->dataLevel = PM_FPA_LEVEL_CHIP; // force load at the CHIP level
+
+    // if MASK or WEIGHT was supplied on command line, bind files to 'load'
+    // the mask and weight will be mosaicked with the image
+    pmFPAfileBindFromArgs (&status, load, config, "PSPHOT.MASK",   "MASK");
+    if (!status) {
+	psError (PS_ERR_UNKNOWN, false, "failed to load find definition");
+	return NULL;
+    }
+
+    pmFPAfileBindFromArgs (&status, load, config, "PSPHOT.WEIGHT", "WEIGHT");
+    if (!status) {
+	psError (PS_ERR_UNKNOWN, false, "failed to load find definition");
+	return NULL;
+    }
+
+    // the psphot analysis is performed on chips
+    pmFPAfile *input = pmFPAfileDefineChipMosaic(config, load->fpa, "PSPHOT.INPUT");
+    if (!input) {
+        psError(PSPHOT_ERR_CONFIG, false, _("Unable to generate new file from PSPHOT.INPUT"));
+        return NULL;
+    }
+
+    // define the additional input/output files associated with psphot 
+    if (!psphotDefineFiles (config, input)) {
+	psError(PSPHOT_ERR_CONFIG, false, "Trouble defining the additional input/output files");
+	return false;
+    }
+
+    // Chip selection: turn on only the chips specified (pass status to suppress missing-key log msg)
+    char *chipLine = psMetadataLookupStr(&status, config->arguments, "CHIP_SELECTIONS");
+    psArray *chips = psStringSplitArray (chipLine, ",", false);
+    if (chips->n > 0) {
+	// select on the basis of extname?
+	pmFPASelectChip (load->fpa, -1, true); // deselect all chips
+	for (int i = 0; i < chips->n; i++) {
+	    int chipNum = atoi(chips->data[i]);
+	    if (! pmFPASelectChip(load->fpa, chipNum, false)) {
+		psError(PSPHOT_ERR_CONFIG, false, "Chip number %d doesn't exist in camera.\n", chipNum);
+		return false;
+	    }
+        }
+    }
+    psFree (chips);
+    psTrace("psphot", 1, "Done with psphotParseCamera...\n");
+
+    psErrorClear();			// some metadata lookup may have failed
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotPetrosian.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotPetrosian.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotPetrosian.c	(revision 15358)
@@ -0,0 +1,8 @@
+# include "psphot.h"
+
+bool psphotPetrosian (pmSource *source, psMetadata *recipe, psMaskType maskVal) {
+
+  psLogMsg ("psphot", PS_LOG_INFO, "not implemented\n");
+  return true;
+
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotRadialPlot.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotRadialPlot.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotRadialPlot.c	(revision 15358)
@@ -0,0 +1,118 @@
+# include "psphotInternal.h"
+
+// this variable is defined in psmodules.h if ohana-config is found
+# if (HAVE_KAPA)
+
+# include <kapa.h>
+
+static int nCount = 0;
+
+bool psphotRadialPlot (int *kapa, const char *filename, pmSource *source) {
+
+    Graphdata graphdata;
+
+    // only plot 50 stars for now...
+    if (nCount > 00) {
+	if (*kapa != 0) {
+	    KiiClose (*kapa);
+	    *kapa = 0;
+	}
+	return true;
+    }
+
+    // XXX get the 'showWindow' option from the recipes somewhere
+    // XXX 'showWindow = false' is broken
+    if (*kapa == 0) {
+	*kapa = pmKapaOpen (false);
+	KapaResize (*kapa, 500, 500);
+	unlink (filename);
+    }
+    if (*kapa == -1) {
+	psError(PSPHOT_ERR_UNKNOWN, true, "failure to open kapa");
+	return false;
+    }
+
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (*kapa);
+
+    // examine sources to set data range
+    graphdata.xmin =  -0.05;
+    graphdata.xmax = +30.05;
+    graphdata.ymin = -0.05;
+    graphdata.ymax = +5.05;
+    KapaSetLimits (*kapa, &graphdata);
+  
+    KapaSetFont (*kapa, "helvetica", 14);
+    KapaBox (*kapa, &graphdata);
+    KapaSendLabel (*kapa, "radius (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (*kapa, "log flux (counts)", KAPA_LABEL_YM);
+	       
+    int nPts = source->pixels->numRows * source->pixels->numCols;
+    psVector *rg = psVectorAllocEmpty (nPts, PS_TYPE_F32);
+    psVector *fg = psVectorAllocEmpty (nPts, PS_TYPE_F32);
+    psVector *rb = psVectorAllocEmpty (nPts, PS_TYPE_F32);
+    psVector *fb = psVectorAllocEmpty (nPts, PS_TYPE_F32);
+
+    int ng = 0;
+    int nb = 0;
+    float Xo = source->modelPSF->params->data.F32[PM_PAR_XPOS] - source->pixels->col0;
+    float Yo = source->modelPSF->params->data.F32[PM_PAR_YPOS] - source->pixels->row0;
+    for (int iy = 0; iy < source->pixels->numRows; iy++) {
+	for (int ix = 0; ix < source->pixels->numCols; ix++) {
+	    if (source->maskObj->data.U8[iy][ix]) {
+		rb->data.F32[nb] = hypot (ix - Xo, iy - Yo) ;
+		fb->data.F32[nb] = log10(source->pixels->data.F32[iy][ix]);
+		nb++;
+	    } else {
+		rg->data.F32[ng] = hypot (ix - Xo, iy - Yo) ;
+		fg->data.F32[ng] = log10(source->pixels->data.F32[iy][ix]);
+		ng++;
+	    }
+	}
+    }
+  
+    // set the plot range here based on lflux, radius
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 2;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot (*kapa, ng, &graphdata);
+    KapaPlotVector (*kapa, ng, rg->data.F32, "x");
+    KapaPlotVector (*kapa, ng, fg->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 0;
+    graphdata.size = 0.3;
+    graphdata.style = 2;
+    KapaPrepPlot (*kapa, nb, &graphdata);
+    KapaPlotVector (*kapa, nb, rb->data.F32, "x");
+    KapaPlotVector (*kapa, nb, fb->data.F32, "y");
+  
+    psLogMsg ("psphot", 3, "saving plot to %s", filename);
+
+    char pagename[16];
+    sprintf (pagename, "%02d", nCount);
+    if (nCount == 0) {
+	KiiPS (*kapa, filename, false, KAPA_PS_NEWPLOT, pagename);
+    } else {
+	KiiPS (*kapa, filename, false, KAPA_PS_NEWPAGE, pagename);
+    }
+
+    psFree (rg);
+    psFree (fg);
+    psFree (rb);
+    psFree (fb);
+
+    nCount ++;
+    return true;
+}
+
+# else
+
+bool psphotRadialPlot (int *kapa, const char *filename, pmSource *source) {
+    psLogMsg ("psphot", 3, "skipping source radial plots");
+    return true;
+}
+
+# endif
Index: /branches/eam_branch_20071023/psphot/src/psphotRadialProfile.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotRadialProfile.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotRadialProfile.c	(revision 15358)
@@ -0,0 +1,45 @@
+# include "psphotInternal.h"
+
+bool psphotRadialProfile (pmSource *source, psMetadata *recipe, psMaskType maskVal) {
+
+    // allocate pmSourceExtendedParameters, if not already defined
+    if (!source->extpars) {
+	source->extpars = pmSourceExtendedParametersAlloc ();
+    }
+
+    if (!source->extpars->profile) {
+	source->extpars->profile = pmSourceRadialProfile (); 
+    }    
+    
+    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->weight = psVectorAllocEmpty (nPts, PS_TYPE_F32);
+
+    psVector *radius = source->extpars->profile->radius;
+    psVector *flux   = source->extpars->profile->flux;
+    psVector *weight = source->extpars->profile->radius;
+
+    // XXX use the extended source model here for Xo, Yo?
+    // XXX define a radius scaled to the elliptical contour?
+
+    int n = 0;
+    float Xo = source->modelEXT->params->data.F32[PM_PAR_XPOS] - source->pixels->col0;
+    float Yo = source->modelEXT->params->data.F32[PM_PAR_YPOS] - source->pixels->row0;
+    for (int iy = 0; iy < source->pixels->numRows; iy++) {
+	for (int ix = 0; ix < source->pixels->numCols; ix++) {
+	    if (source->maskObj->data.U8[iy][ix]) continue;
+	    radius->data.F32[n] = hypot (ix - Xo, iy - Yo) ;
+	    flux->data.F32[n]   = source->pixels->data.F32[iy][ix];
+	    weight->data.F32[n] = source->weight->data.F32[iy][ix];
+	    n++;
+	}
+    }
+    radius->n = n;
+    weight->n = n;
+    flux->n = n;
+
+    SortVectorsByRadius (radius, flux, weight);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotRadiusChecks.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotRadiusChecks.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotRadiusChecks.c	(revision 15358)
@@ -0,0 +1,106 @@
+# include "psphotInternal.h"
+# define RADIUS_TYPE int
+
+static float PSF_FIT_NSIGMA;
+static float PSF_FIT_PADDING;
+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 status = true;
+
+    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");
+
+    return true;
+}
+
+// call this function whenever you (re)-define the PSF model
+bool psphotCheckRadiusPSF (pmReadout *readout, pmSource *source, pmModel *model)
+{
+    psF32 *PAR = model->params->data.F32;
+
+    // XXX do we have a better value for the sky noise level?  not really...
+    pmMoments *moments = source->moments;
+
+    // set the fit radius based on the object flux limit and the model
+    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->radiusFit = (RADIUS_TYPE)(radiusFit + PSF_FIT_PADDING);
+
+    if (isnan(model->radiusFit)) psAbort("error in radius");
+	
+    if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+	model->radiusFit *= 2;
+    }
+
+    bool status = pmSourceRedefinePixels (source, readout, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit);
+
+    // set the mask to flag the excluded pixels
+    psImageKeepCircle (source->maskObj, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit, "OR", PM_MASK_MARK);
+    return status;
+}
+
+bool psphotCheckRadiusPSFBlend (pmReadout *readout, pmSource *source, pmModel *model, float dR) {
+
+    psF32 *PAR = model->params->data.F32;
+
+    pmMoments *moments = source->moments;
+    if (moments == NULL) return false;
+
+    // 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");
+	
+    if (source->mode &  PM_SOURCE_MODE_SATSTAR) {
+	model->radiusFit *= 2;
+    }
+
+    bool status = pmSourceRedefinePixels (source, readout, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit);
+
+    // set the mask to flag the excluded pixels
+    psImageKeepCircle (source->maskObj, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit, "OR", PM_MASK_MARK);
+    return status;
+}
+
+static float EXT_FIT_NSIGMA;
+static float EXT_FIT_PADDING;
+
+bool psphotInitRadiusEXT (psMetadata *recipe, pmModelType type) {
+
+    bool status;
+
+    EXT_FIT_NSIGMA   = psMetadataLookupF32 (&status, recipe, "EXT_FIT_NSIGMA");
+    EXT_FIT_PADDING  = psMetadataLookupF32 (&status, recipe, "EXT_FIT_PADDING");
+
+    return true;
+}
+
+// call this function whenever you (re)-define the EXT model
+bool psphotCheckRadiusEXT (pmReadout *readout, pmSource *source, pmModel *model) {
+
+    psF32 *PAR = model->params->data.F32;
+
+    pmMoments *moments = source->moments;
+    if (moments == NULL) return false;
+
+    // set the fit radius based on the object flux limit and the model
+    model->radiusFit = (RADIUS_TYPE) (model->modelRadius (model->params, EXT_FIT_NSIGMA*moments->dSky) + EXT_FIT_PADDING);
+    if (isnan(model->radiusFit)) psAbort("error in radius");
+
+    // redefine the pixels if needed
+    bool status = pmSourceRedefinePixels (source, readout, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit);
+
+    // set the mask to flag the excluded pixels
+    psImageKeepCircle (source->maskObj, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit, "OR", PM_MASK_MARK);
+    return status;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotReadout.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotReadout.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotReadout.c	(revision 15358)
@@ -0,0 +1,291 @@
+# include "psphotInternal.h"
+
+bool psphotReadout(pmConfig *config, const pmFPAview *view) {
+
+    bool dump = (psTraceGetLevel("psphot") >= 6);
+
+    // 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;
+    }
+
+    // Interpret the mask values
+    const char *maskValStr = psMetadataLookupStr(NULL, recipe, "MASKVAL"); // String with mask names
+    if (!maskValStr) {
+        psError(PSPHOT_ERR_CONFIG, false, "Missing recipe item: MASKVAL(STR)");
+        return false;
+    }
+    psMaskType maskVal = pmConfigMask(maskValStr, config); // Mask values to mask against
+    psMaskType maskMark = pmConfigMask("MARK", config); // Mask value for marking
+    psMaskType maskSat = pmConfigMask("SAT", config); // Mask value for saturated pixels
+    psMaskType maskBad = pmConfigMask("BAD", config); // Mask value for bad pixels
+
+    // 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);
+
+    // Use the new pmFootprints approach?
+    const bool useFootprints = psMetadataLookupBool(NULL, recipe, "USE_FOOTPRINTS");
+
+    // generate mask & weight images if they don't already exit
+    if (!readout->mask) {
+        if (!pmReadoutGenerateMask(readout, maskSat, maskBad)) {
+            psError (PSPHOT_ERR_CONFIG, false, "trouble creating mask");
+            return false;
+        }
+    }
+    if (!readout->weight) {
+        if (!pmReadoutGenerateWeight(readout, true)) {
+            psError (PSPHOT_ERR_CONFIG, false, "trouble creating weight");
+            return false;
+        }
+    }
+
+    // set the photcode for this image
+    if (!psphotAddPhotcode (recipe, config, view)) {
+        psError (PSPHOT_ERR_CONFIG, false, "trouble defining the photcode");
+        return false;
+    }
+
+    // I have a valid mask, now mask in the analysis region of interest
+    psphotMaskReadout (readout, recipe, maskBad);
+
+    if (psTraceGetLevel("psphot") >= 5) {
+        psphotSaveImage (NULL, readout->image,  "image.fits");
+        psphotSaveImage (NULL, readout->mask,   "mask.fits");
+        psphotSaveImage (NULL, readout->weight, "weight.fits");
+    }
+
+    if (!strcasecmp (breakPt, "NOTHING")) {
+        return psphotReadoutCleanup(config, readout, recipe, NULL, NULL);
+    }
+
+    // generate a background model (median, smoothed image)
+    if (!psphotImageMedian (config, view, maskVal)) {
+        return psphotReadoutCleanup (config, readout, recipe, NULL, NULL);
+    }
+
+    if (!strcasecmp (breakPt, "BACKMDL")) {
+        return psphotReadoutCleanup (config, readout, recipe, NULL, NULL);
+    }
+
+    // run a single-model test if desired
+    psphotModelTest (config, view, recipe, maskVal, maskMark);
+
+    // 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);
+
+    // find the peaks in the image.
+
+    // XXX clean this up into a single function.  if psf is defined, we should treat this as
+    // pass "2", not "1".  re-define this boolean to be "have PSF"
+    psArray *peaks;
+    psArray *footprints = NULL;
+    if (useFootprints) {
+       footprints = psphotFindPeaks (readout, recipe, useFootprints, 1, maskVal);
+       int growRadius = psMetadataLookupS32(NULL, recipe, "FOOTPRINT_GROW_RADIUS");
+       if (growRadius > 0) {
+           psArray *tmp = pmGrowFootprintArray(footprints, growRadius);
+           psFree(footprints);
+           footprints = tmp;
+       }
+       psphotCullPeaks(readout->image, readout->weight, recipe, footprints);
+
+       peaks = pmFootprintArrayToPeaks(footprints);
+    } else {
+       peaks = psphotFindPeaks (readout, recipe, useFootprints, 1, maskVal);
+    }
+
+    if (!peaks) {
+        psLogMsg ("psphot", 3, "unable to find peaks in this image");
+        return psphotReadoutCleanup (config, readout, recipe, NULL, NULL);
+    }
+
+    // construct sources and measure basic stats
+    psArray *sources = psphotSourceStats (readout, recipe, peaks, maskVal, maskMark);
+    if (!sources) return false;
+    psFree (peaks);
+
+    if (!strcasecmp (breakPt, "PEAKS")) {
+        return psphotReadoutCleanup(config, readout, recipe, NULL, sources);
+    }
+
+    psphotDeblendSatstars (sources, recipe);
+
+    // mark blended peaks PS_SOURCE_BLEND
+    if (!psphotBasicDeblend (sources, recipe)) {
+        psLogMsg ("psphot", 3, "failed on deblend analysis");
+        return psphotReadoutCleanup (config, readout, recipe, NULL, sources);
+    }
+
+    // classify sources based on moments, brightness
+    if (!psphotRoughClass (sources, recipe, havePSF, maskSat)) {
+        psLogMsg ("psphot", 3, "failed to find a valid PSF clump for image");
+        return psphotReadoutCleanup (config, readout, recipe, NULL, sources);
+    }
+    if (!strcasecmp (breakPt, "MOMENTS")) {
+        return psphotReadoutCleanup(config, readout, recipe, NULL, 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, maskVal, maskMark);
+        if (psf == NULL) {
+            psLogMsg ("psphot", 3, "failure to construct a psf model");
+            return psphotReadoutCleanup (config, readout, recipe, psf, sources);
+        }
+        havePSF = true;
+    }
+
+    // XXX Test dump of PSF parameters across image field.
+
+    if (!strcasecmp (breakPt, "PSFMODEL")) {
+        return psphotReadoutCleanup (config, readout, recipe, psf, sources);
+    }
+
+    // include externally-supplied sources
+    psphotLoadExtSources (config, view, sources);
+
+    // construct an initial model for each object
+    psphotGuessModels (readout, sources, recipe, psf, maskVal);
+
+    // XXX test output of models
+    // psphotTestSourceOutput (readout, sources, recipe, psf);
+
+    if (dump) psphotSaveImage (NULL, readout->image,  "image.v0.fits");
+
+    // linear PSF fit to peaks
+    psphotFitSourcesLinear (readout, sources, recipe, psf, FALSE, maskVal);
+    if (dump) psphotSaveImage (NULL, readout->image,  "image.v1.fits");
+
+    // XXX test the CR/EXT measurement
+    // XXX we need to call this here and after the second pass: option for starting number?
+    psphotSourceSize (readout, sources, recipe);
+
+    if (!strcasecmp (breakPt, "ENSEMBLE")) {
+        goto finish;
+    }
+
+    // non-linear PSF and EXT fit to brighter sources
+    psphotBlendFit (readout, sources, recipe, psf, maskVal);
+    if (dump) psphotSaveImage (NULL, readout->image,  "image.v2.fits");
+
+    // replace all sources
+    psphotReplaceAll (sources, maskVal);
+    if (dump) psphotSaveImage (NULL, readout->image,  "image.v3.fits");
+
+    // linear PSF fit to remaining peaks
+    psphotFitSourcesLinear (readout, sources, recipe, psf, TRUE, maskVal);
+    if (dump) psphotSaveImage (NULL, readout->image,  "image.v4.fits");
+    if (!strcasecmp (breakPt, "PASS1")) {
+        goto finish;
+    }
+
+    // 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);
+
+    // add noise for subtracted objects
+    psphotAddNoise (readout, sources, recipe, true, maskVal);
+
+    // find the peaks in the image
+    psArray *newPeaks;
+    if (useFootprints) {
+       psArray *newFootprints = psphotFindPeaks (readout, recipe, useFootprints, 2, maskVal);
+
+       int growRadius = psMetadataLookupS32(NULL, recipe, "FOOTPRINT_GROW_RADIUS_2");
+       if (growRadius > 0) {
+           psArray *tmp = pmGrowFootprintArray(newFootprints, growRadius);
+           psFree(newFootprints);
+           newFootprints = tmp;
+       }
+
+       // merge in old peaks
+       const int includePeaks = 0x0 | 0x2; // i.e. just from newFootprints
+       psArray *mergedFootprints = pmMergeFootprintArrays(footprints, newFootprints, includePeaks);
+       psFree(footprints);
+       psFree(newFootprints);
+
+       psphotCullPeaks(readout->image, readout->weight, recipe, mergedFootprints);
+
+       newPeaks = pmFootprintArrayToPeaks(mergedFootprints);
+
+       psFree(mergedFootprints);
+    } else {
+       newPeaks = psphotFindPeaks(readout, recipe, useFootprints, 2, maskVal);
+    }
+
+    // remove noise for subtracted objects
+    psphotAddNoise (readout, sources, recipe, false, maskVal);
+
+    // define new sources based on the new peaks
+    psArray *newSources = psphotSourceStats (readout, recipe, newPeaks, maskVal, maskMark);
+    psFree (newPeaks);
+
+    // set source type
+    if (!psphotRoughClass (newSources, recipe, havePSF, maskSat)) {
+        psLogMsg ("psphot", 3, "failed to find a valid PSF clump for image");
+        return psphotReadoutCleanup (config, readout, recipe, psf, sources);
+    }
+
+    // create full input models
+    psphotGuessModels (readout, newSources, recipe, psf, maskVal);
+
+    // replace all sources
+    psphotReplaceAll (sources, maskVal);
+    if (dump) psphotSaveImage (NULL, readout->image,  "image.v5.fits");
+
+    // merge the newly selected peaks into the existing list
+    psphotMergeSources (sources, newSources);
+    psFree (newSources);
+
+    // linear PSF fit to remaining peaks
+    psphotFitSourcesLinear (readout, sources, recipe, psf, TRUE, maskVal);
+    if (dump) psphotSaveImage (NULL, readout->image,  "image.v6.fits");
+
+    psphotExtendedSources (readout, sources, recipe, maskVal);
+
+finish:
+
+    // plot positive sources
+    if (!havePSF) {
+        // psphotSourcePlots (readout, sources, recipe, maskVal);
+    }
+
+    // measure aperture photometry corrections
+    if (!havePSF && !psphotApResid (readout, sources, recipe, psf, maskVal, maskMark)) {
+        psTrace ("psphot", 4, "failure on psphotApResid");
+        psError(PSPHOT_ERR_PHOTOM, false, "Measure aperture photometry corrections");
+        return false;
+    }
+
+    // calculate source magnitudes
+    pmReadout *background = psphotSelectBackground (config, view, false);
+    psphotMagnitudes(sources, recipe, psf, background, maskVal, maskMark);
+
+    // replace failed sources?
+    // psphotReplaceUnfit (sources);
+
+    // replace background in residual image
+    psphotSkyReplace (config, view);
+
+    // drop the references to the image pixels held by each source
+    psphotSourceFreePixels (sources);
+
+    // create the exported-metadata and free local data
+    return psphotReadoutCleanup(config, readout, recipe, psf, sources);
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotReadoutCleanup.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotReadoutCleanup.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotReadoutCleanup.c	(revision 15358)
@@ -0,0 +1,80 @@
+# 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, pmPSF *psf, psArray *sources) {
+
+    // remove internal pmFPAfiles, if created
+    bool 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");
+	return false;
+    }
+
+    if (psErrorCodeLast() == PSPHOT_ERR_DATA) {
+        psErrorStackPrint(stderr, "Error in the psphot readout analysis");
+	psErrorClear();
+    } 
+    if (psErrorCodeLast() != PS_ERR_NONE) { 
+	psFree (psf);
+	psFree (sources);
+	return false;
+    }
+
+    // use the psf-model to measure FWHM stats
+    if (psf) {
+        if (!psphotPSFstats (readout, recipe, psf)) {
+            psError(PSPHOT_ERR_PROG, false, "Failed to measure PSF shape parameters");
+	    psFree (psf);
+	    psFree (sources);
+            return false;
+        }
+    }
+    // otherwise, use the source moments to measure FWHM stats
+    if (!psf && sources) {
+        if (!psphotMomentsStats (readout, recipe, sources)) {
+            psError(PSPHOT_ERR_PROG, false, "Failed to measure Moment shape parameters");
+	    psFree (sources);
+            return false;
+        }
+    }
+
+    // write NSTARS to the image header
+    psphotSetHeaderNstars (recipe, sources);
+
+    // create an output header with stats results
+    psMetadata *header = psphotDefineHeader (recipe);
+
+    // save the results of the analysis
+    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) {
+	// 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
+	// are required to save it on the chip, but this will cause problems if we ever want to
+	// run psphot on an unmosaiced image
+	pmCell *cell = readout->parent;
+	pmChip *chip = cell->parent;
+	psMetadataAdd (chip->analysis, PS_LIST_TAIL, "PSPHOT.PSF", PS_DATA_UNKNOWN | PS_META_REPLACE,  "psphot psf", psf);
+    }
+
+    if (psErrorCodeLast() != PS_ERR_NONE) {
+        psErrorStackPrint(stderr, "unexpected remaining errors");
+	abort();
+    }
+
+    // XXX move this to top of loop?
+    pmKapaClose ();
+
+    psFree (psf);
+    psFree (header);
+    psFree (sources);
+
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotReplaceUnfit.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotReplaceUnfit.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotReplaceUnfit.c	(revision 15358)
@@ -0,0 +1,87 @@
+# include "psphotInternal.h"
+
+// replace the flux for sources which failed
+bool psphotReplaceUnfit (psArray *sources, psMaskType maskVal) {
+
+    pmSource *source;
+
+    psTimerStart ("psphot");
+
+    for (int i = 0; i < sources->n; i++) {
+      source = sources->data[i];
+
+      // replace other sources?
+      if (source->mode & PM_SOURCE_MODE_FAIL) goto replace;
+      continue;
+
+    replace:
+        pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
+        source->mode &= ~PM_SOURCE_MODE_SUBTRACTED;
+        source->mode &= ~PM_SOURCE_MODE_TEMPSUB;
+    }
+    psLogMsg ("psphot.replace", 3, "replace unfitted models: %f sec (%ld objects)\n", psTimerMark ("psphot"), sources->n);
+    return true;
+}
+
+bool psphotReplaceAll (psArray *sources, psMaskType maskVal) {
+
+    pmSource *source;
+
+    psTimerStart ("psphot");
+
+    for (int i = 0; i < sources->n; i++) {
+      source = sources->data[i];
+
+      // replace other sources?
+      if (!(source->mode & PM_SOURCE_MODE_SUBTRACTED)) continue;
+
+      pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
+      source->mode &= ~PM_SOURCE_MODE_SUBTRACTED;
+    }
+    psLogMsg ("psphot.replace", PS_LOG_INFO, "replace models for %ld objects: %f sec\n", sources->n, psTimerMark ("psphot"));
+    return true;
+}
+
+// add source, if the source has been subtracted (or if we ignore the state)
+bool psphotAddWithTest (pmSource *source, bool useState, psMaskType maskVal) {
+
+    // what is current state? (true : add; false : sub)
+    bool state = !(source->mode & PM_SOURCE_MODE_SUBTRACTED);
+    if (state && useState) return true;
+
+    // replace the model if 1) state says it is missing or 2) useState is false (just do it)
+    if (!state || !useState) {
+        pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
+    }
+    return true;
+}
+
+// sub source, if the source has been added (or if we ignore the state)
+bool psphotSubWithTest (pmSource *source, bool useState, psMaskType maskVal) {
+
+    // what is current state? (true : sub; false : add)
+    bool state = (source->mode & PM_SOURCE_MODE_SUBTRACTED);
+    if (state && useState) return true;
+
+    // replace the model if 1) state says it is missing or 2) useState is false (just do it)
+    if (!state || !useState) {
+        pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
+    }
+    return true;
+}
+
+// add or sub source replace or if the source has
+bool psphotSetState (pmSource *source, bool curState, psMaskType maskVal) {
+
+    // what is desired state? (true : add; false : sub)
+    bool newState = !(source->mode & PM_SOURCE_MODE_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/eam_branch_20071023/psphot/src/psphotRoughClass.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotRoughClass.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotRoughClass.c	(revision 15358)
@@ -0,0 +1,55 @@
+# include "psphotInternal.h"
+# define CHECK_STATUS(S,MSG) { \
+  if (!status) { \
+    psError(PSPHOT_ERR_CONFIG, false, "missing PSF Clump entry: %s\n", MSG); \
+    return false; \
+  } }
+
+// 2006.02.02 : no leaks
+bool psphotRoughClass (psArray *sources, psMetadata *recipe, const bool havePSF, psMaskType maskSat) {
+
+    bool status;
+    static pmPSFClump psfClump;
+
+    psTimerStart ("psphot");
+
+    if (!havePSF) {
+        // determine the PSF parameters from the source moment values
+        psfClump = pmSourcePSFClump (sources, recipe);
+	psMetadataAddF32 (recipe, PS_LIST_TAIL, "PSF.CLUMP.X",  PS_META_REPLACE, "psf clump center", psfClump.X);
+	psMetadataAddF32 (recipe, PS_LIST_TAIL, "PSF.CLUMP.Y",  PS_META_REPLACE, "psf clump center", psfClump.Y);
+	psMetadataAddF32 (recipe, PS_LIST_TAIL, "PSF.CLUMP.DX", PS_META_REPLACE, "psf clump center", psfClump.dX);
+	psMetadataAddF32 (recipe, 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, recipe, "PSF.CLUMP.X");  CHECK_STATUS (status, "PSF.CLUMP.X");
+        psfClump.Y  = psMetadataLookupF32 (&status, recipe, "PSF.CLUMP.Y");  CHECK_STATUS (status, "PSF.CLUMP.Y");
+        psfClump.dX = psMetadataLookupF32 (&status, recipe, "PSF.CLUMP.DX");  CHECK_STATUS (status, "PSF.CLUMP.DX");
+        psfClump.dY = psMetadataLookupF32 (&status, recipe, "PSF.CLUMP.DY");  CHECK_STATUS (status, "PSF.CLUMP.DY");
+    }
+
+    if (psfClump.X < 0) {
+        psError(PSPHOT_ERR_PROG, false, "programming error calling pmSourcePSFClump");
+        return false;
+    }
+    if (!psfClump.X || !psfClump.Y) {
+        psError(PSPHOT_ERR_DATA, true, "Failed to find a valid PSF clump");
+        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);
+
+    // group into STAR, COSMIC, EXTENDED, SATURATED, etc.
+    if (!pmSourceRoughClass (sources, recipe, psfClump, maskSat)) {
+        psError(PSPHOT_ERR_PROG, false, "programming error calling pmSourceRoughClass");
+        return false;
+    }
+
+    // optional printout of source moments only
+    psphotDumpMoments (recipe, sources);
+
+    psLogMsg ("psphot.roughclass", PS_LOG_INFO, "rough classification: %f sec\n", psTimerMark ("psphot"));
+
+    return true;
+}
+
Index: /branches/eam_branch_20071023/psphot/src/psphotSavePSFStars.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotSavePSFStars.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotSavePSFStars.c	(revision 15358)
@@ -0,0 +1,148 @@
+# include "psphotInternal.h"
+
+bool psphotSourcePlots (pmReadout *readout, psArray *sources, psMetadata *recipe) {
+
+    // bool status = false;
+    psTimerStart ("psphot");
+
+    // if this file is defined, create the necessary output data
+    pmFPAfile *file = psMetadataLookupPtr(&status, config->files, "PSPHOT.PSF.STARS");
+
+    // the source images are written to an image 10x the size of a PSF object
+    // float OUTER = psMetadataLookupF32 (&status, recipe, "SKY_OUTER_RADIUS");
+    // PS_ASSERT (status, false);
+
+    int DX = 21;
+    int DY = 21;
+
+    // examine PSF sources in S/N order (brightest first)
+    sources = psArraySort (sources, pmSourceSortBySN);
+
+    // counters to track the size of the image and area used in a row
+    int dX = 0;				// starting corner of next box
+    int dY = 0;				// height of row so far
+    int NX = 20*DX;			// full width of output image
+    int NY = 0;				// total height of output image
+
+    // first, examine the PSF and SAT stars:
+    // - determine bounding boxes for summary image
+    for (int i = 0; i < sources->n; i++) {
+
+        pmSource *source = sources->data[i];
+
+	bool keep = false;
+        keep |= (source->mode & PM_SOURCE_MODE_PSFSTAR);
+        keep |= (source->mode & PM_SOURCE_MODE_SATSTAR);
+	if (!keep) continue;
+
+	// how does this subimage get placed into the output image?
+	// DX = source->pixels->numCols
+	// DY = source->pixels->numRows
+
+	if (dX + DX > NX) {
+	    // too wide for the rest of this row
+	    if (dX == 0) {
+		// alone on this row
+		NY += DY;
+		dX = 0;
+		dY = 0;
+	    } else {
+		// start the next row
+		NY += dY;
+		dX = DX;
+		dY = DY;
+	    }
+	} else {
+	    // extend this row
+	    dX += DX;
+	    dY = PS_MAX (dY, DY);
+	}
+    }
+
+    // allocate output image
+    psImage *outpos = psImageAlloc (NX, NY, PS_TYPE_F32);
+    psImage *outsub = psImageAlloc (NX, NY, PS_TYPE_F32);
+
+    int Xo = 0;				// starting corner of next box
+    int Yo = 0;				// starting corner of next box
+    dY = 0;				// height of row so far
+
+    int nPSF = 0;
+    int nSAT = 0;
+    int kapa = 0;			// file descriptor for plotting routine
+
+    // first, examine the PSF and SAT stars:
+    // - generate radial plots (PS plots)
+    // - create output image array
+    for (int i = 0; i < sources->n; i++) {
+
+        pmSource *source = sources->data[i];
+
+	bool keep = false;
+        if (source->mode & PM_SOURCE_MODE_PSFSTAR) {
+	    nPSF ++;
+	    keep = true;
+	}
+        if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+	    nSAT ++;
+	    keep = true;
+	}	    
+	if (!keep) continue;
+
+	// how does this subimage get placed into the output image?
+	// DX = source->pixels->numCols
+	// DY = source->pixels->numRows
+
+	if (Xo + DX > NX) {
+	    // too wide for the rest of this row
+	    if (Xo == 0) {
+		// place source alone on this row
+		psphotAddWithTest (source, true); // replace source if subtracted
+		psphotRadialPlot (&kapa, "radial.plots.ps", source);
+		psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY);
+
+		psphotSubWithTest (source, false); // remove source (force)
+		psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY);
+
+		psphotSetState (source, false); // replace source (has been subtracted)
+		Yo += DY;
+		Xo = 0;
+		dY = 0;
+	    } else {
+		// start the next row
+		Yo += dY;
+		Xo = 0;
+		psphotAddWithTest (source, true); // replace source if subtracted
+		psphotRadialPlot (&kapa, "radial.plots.ps", source);
+		psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY);
+
+		psphotSubWithTest (source, false); // remove source (force)
+		psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY);
+		psphotSetState (source, false); // replace source (has been subtracted)
+
+		Xo = DX;
+		dY = DY;
+	    }
+	} else {
+	    // extend this row
+	    psphotAddWithTest (source, true); // replace source if subtracted
+	    psphotRadialPlot (&kapa, "radial.plots.ps", source);
+	    psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY);
+
+	    psphotSubWithTest (source, false); // remove source (force)
+	    psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY);
+	    psphotSetState (source, false); // replace source (has been subtracted)
+
+	    Xo += DX;
+	    dY = PS_MAX (dY, DY);
+	}
+    }
+
+    psphotSaveImage (NULL, outpos, "outpos.fits");
+    psphotSaveImage (NULL, outsub, "outsub.fits");
+    psLogMsg ("psphot", PS_LOG_INFO, "plotted %d sources (%d psf, %d sat): %f sec\n", nPSF + nSAT, nPSF, nSAT, psTimerMark ("psphot"));
+
+    psFree (outpos);
+    psFree (outsub);
+    return (sources);
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotSkyReplace.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotSkyReplace.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotSkyReplace.c	(revision 15358)
@@ -0,0 +1,33 @@
+# include "psphotInternal.h"
+
+// 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) {
+
+    psTimerStart ("psphot");
+
+    // find the currently selected readout
+    pmReadout *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
+    if (readout == NULL) psAbort("input not defined");
+
+    // select background pixels, from output background file, or create
+    pmReadout *background = pmFPAfileThisReadout (config->files, view, "PSPHOT.BACKGND");
+    if (background == NULL) psAbort("background not defined");
+
+    // select the corresponding images
+    psF32 **image = readout->image->data.F32;
+    psU8  **mask  = readout->mask->data.U8;
+    psF32 **back  = background->image->data.F32;
+
+    // replace the background model
+    for (int j = 0; j < readout->image->numRows; j++) {
+        for (int i = 0; i < readout->image->numCols; i++) {
+            if (!mask[j][i]) {
+                image[j][i] += back[j][i];
+            }
+        }
+    }
+    psLogMsg ("psphot.sky", PS_LOG_DETAIL, "replace background flux : %f sec\n", psTimerMark ("psphot"));
+    return true;
+}
+
Index: /branches/eam_branch_20071023/psphot/src/psphotSortBySN.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotSortBySN.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotSortBySN.c	(revision 15358)
@@ -0,0 +1,1 @@
+
Index: /branches/eam_branch_20071023/psphot/src/psphotSourceFits.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotSourceFits.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotSourceFits.c	(revision 15358)
@@ -0,0 +1,378 @@
+# include "psphotInternal.h"
+
+// given a source with an existing modelPSF, attempt a full PSF fit, subtract if successful
+// XXX this function does not call pmSourceFitModelInit : fix this?
+
+static int NfitPSF = 0;
+static int NfitBlend = 0;
+static int NfitDBL = 0;
+static int NfitEXT = 0;
+
+bool psphotFitInit () {
+    psTimerStart ("psphot.fits");
+    return true;
+}
+
+bool psphotFitSummary () {
+
+    fprintf (stderr, "fitted %5d psf, %5d blend, %5d ext, %5d dbl : %6.2f sec\n",
+             NfitPSF, NfitBlend, NfitEXT, NfitDBL, psTimerMark ("psphot.fits"));
+    return true;
+}
+
+bool psphotFitBlend (pmReadout *readout, pmSource *source, pmPSF *psf, psMaskType maskVal) {
+
+    float x, y, dR;
+
+    // if this source is not a possible blend, just fit as PSF
+    if ((source->blends == NULL) || (source->mode & PM_SOURCE_MODE_SATSTAR)) {
+        bool status = psphotFitPSF (readout, source, psf, maskVal);
+        return status;
+    }
+
+    // save the PSF model from the Ensemble fit
+    pmModel *PSF = pmModelCopy (source->modelPSF);
+
+    if (isnan(PSF->params->data.F32[PM_PAR_I0])) psAbort("nan in blend fit primary");
+
+    x = PSF->params->data.F32[PM_PAR_XPOS];
+    y = PSF->params->data.F32[PM_PAR_YPOS];
+
+    psArray *modelSet = psArrayAllocEmpty (source->blends->n + 1);
+    psArrayAdd (modelSet, 16, PSF);
+
+    psArray *sourceSet = psArrayAllocEmpty (source->blends->n + 1);
+    psArrayAdd (sourceSet, 16, source);
+
+    // we need to include all blends in the fit (unless primary is saturated?)
+    dR = 0;
+    for (int i = 0; i < source->blends->n; i++) {
+        pmSource *blend = source->blends->data[i];
+
+        // find the blend which is furthest from source
+        dR = PS_MAX (dR, hypot (blend->peak->xf - x, blend->peak->yf - y));
+
+        // create the model and guess parameters for this blend
+        pmModel *model = pmModelAlloc (PSF->type);
+        for (int j = 0; j < model->params->n; j++) {
+            model->params->data.F32[j] = PSF->params->data.F32[j];
+            model->dparams->data.F32[j] = PSF->dparams->data.F32[j];
+        }
+
+        // XXX assume local sky is 0.0?
+        model->params->data.F32[PM_PAR_I0] = blend->peak->flux;
+        model->params->data.F32[PM_PAR_XPOS] = blend->peak->xf;
+        model->params->data.F32[PM_PAR_YPOS] = blend->peak->yf;
+
+        // these should never be invalid values
+        // XXX drop these tests eventually
+        if (isnan(model->params->data.F32[PM_PAR_I0]))   psAbort("nan in blend fit");
+        if (isnan(model->params->data.F32[PM_PAR_XPOS])) psAbort("nan in blend fit");
+        if (isnan(model->params->data.F32[PM_PAR_YPOS])) psAbort("nan in blend fit");
+
+        // add this blend to the list
+        psArrayAdd (modelSet, 16, model);
+        psArrayAdd (sourceSet, 16, blend);
+
+        // free to avoid double counting model
+        psFree (model);
+    }
+
+    // extend source radius as needed
+    psphotCheckRadiusPSFBlend (readout, source, PSF, dR);
+
+    // fit PSF model (set/unset the pixel mask)
+    pmSourceFitSet (source, modelSet, PM_SOURCE_FIT_PSF, maskVal);
+
+    // correct model chisq for flux trend
+    double chiTrend = psPolynomial1DEval (psf->ChiTrend, PSF->params->data.F32[PM_PAR_I0]);
+    PSF->chisqNorm = PSF->chisq / chiTrend;
+
+    // evaluate the blend objects, subtract if good, free otherwise
+    for (int i = 1; i < modelSet->n; i++) {
+        pmSource *blend = sourceSet->data[i];
+        pmModel *model  = modelSet->data[i];
+
+        // correct model chisq for flux trend
+        chiTrend = psPolynomial1DEval (psf->ChiTrend, model->params->data.F32[PM_PAR_I0]);
+        model->chisqNorm = model->chisq / chiTrend;
+
+        // if this one failed, skip it
+        if (!psphotEvalPSF (blend, model)) {
+            psTrace ("psphot", 4, "failed on blend %d of %ld\n", i, modelSet->n);
+            continue;
+        }
+
+        // otherwise, supply the resulting model to the corresponding blend
+        psFree(blend->modelPSF);
+        blend->modelPSF = psMemIncrRefCounter (model);
+        psTrace ("psphot", 5, "fitted blend as PSF\n");
+
+        // build cached model and subtract
+        pmSourceCacheModel (blend, maskVal);
+        pmSourceSub (blend, PM_MODEL_OP_FULL, maskVal);
+        blend->mode |=  PM_SOURCE_MODE_SUBTRACTED;
+        blend->mode &= ~PM_SOURCE_MODE_TEMPSUB;
+    }
+    NfitBlend += modelSet->n;
+
+    // evaluate the primary object
+    if (!psphotEvalPSF (source, PSF)) {
+        psTrace ("psphot", 4, "failed on blend 0 of %ld\n", modelSet->n);
+        psFree (PSF);
+        psFree (modelSet);
+        psFree (sourceSet);
+        return false;
+    }
+    psFree (modelSet);
+    psFree (sourceSet);
+
+    // save the new, successful model
+    psFree (source->modelPSF);
+    source->modelPSF = PSF;
+    psTrace ("psphot", 5, "fitted primary as PSF\n");
+
+    // build cached model and subtract
+    pmSourceCacheModel (source, maskVal);
+    pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
+    source->mode |=  PM_SOURCE_MODE_SUBTRACTED;
+    source->mode &= ~PM_SOURCE_MODE_TEMPSUB;
+    return true;
+}
+
+bool psphotFitPSF (pmReadout *readout, pmSource *source, pmPSF *psf, psMaskType maskVal) {
+
+    double chiTrend;
+
+    NfitPSF ++;
+
+    // save the PSF model from the Ensemble fit
+    pmModel *PSF = pmModelCopy (source->modelPSF);
+    if (isnan(PSF->params->data.F32[1])) psAbort("nan in psf fit");
+
+    // extend source radius as needed
+    psphotCheckRadiusPSF (readout, source, PSF);
+
+    // fit PSF model (set/unset the pixel mask)
+    pmSourceFitModel (source, PSF, PM_SOURCE_FIT_PSF, maskVal);
+
+    // correct model chisq for flux trend
+    chiTrend = psPolynomial1DEval (psf->ChiTrend, PSF->params->data.F32[PM_PAR_I0]);
+    PSF->chisqNorm = PSF->chisq / chiTrend;
+
+    // does the PSF model succeed?
+    if (!psphotEvalPSF (source, PSF)) {
+        psFree (PSF);
+        return false;
+    }
+
+    // free old model, save new model
+    psFree (source->modelPSF);
+    source->modelPSF = PSF;
+    psTrace ("psphot", 5, "fitted as PSF\n");
+
+    // build cached model and subtract
+    pmSourceCacheModel (source, maskVal);
+    pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
+    source->mode |=  PM_SOURCE_MODE_SUBTRACTED;
+    source->mode &= ~PM_SOURCE_MODE_TEMPSUB;
+
+    return true;
+}
+
+static float EXT_MIN_SN;
+static float EXT_MOMENTS_RAD;
+static pmModelType modelTypeEXT;
+
+bool psphotInitLimitsEXT (psMetadata *recipe) {
+
+    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
+    char *modelNameEXT = psMetadataLookupStr (&status, recipe, "EXT_MODEL");
+    modelTypeEXT = pmModelClassGetType (modelNameEXT);
+    psphotInitRadiusEXT (recipe, modelTypeEXT);
+
+    return true;
+}
+
+bool psphotFitBlob (pmReadout *readout, pmSource *source, psArray *sources, pmPSF *psf, psMaskType maskVal) {
+
+    bool okEXT, okDBL;
+    float chiEXT, chiDBL;
+    double chiTrend;
+    pmModel *ONE = NULL;
+
+    // skip the source if we don't think it is extended
+    if (source->type == PM_SOURCE_TYPE_UNKNOWN) return false;
+    if (source->type == PM_SOURCE_TYPE_DEFECT) return false;
+    if (source->type == PM_SOURCE_TYPE_SATURATED) return false;
+    if (source->moments->SN < EXT_MIN_SN) return false;
+
+    // recalculate the source moments using the larger extended-source moments radius
+    if (!pmSourceMoments (source, EXT_MOMENTS_RAD)) return false;
+
+    psTrace ("psphot", 5, "trying blob...\n");
+
+    // psTraceSetLevel("psModules.objects.pmSourceFitSet", 5);
+
+    // this temporary source is used as a place-holder by the psphotEval functions below
+    pmSource *tmpSrc = pmSourceAlloc ();
+
+    pmModel *EXT = psphotFitEXT (readout, source, maskVal);
+    okEXT = psphotEvalEXT (tmpSrc, EXT);
+    chiEXT = EXT->chisq / EXT->nDOF;
+
+    psArray *DBL = psphotFitDBL (readout, source, maskVal);
+    okDBL  = psphotEvalDBL (tmpSrc, DBL->data[0]);
+    okDBL &= psphotEvalDBL (tmpSrc, DBL->data[1]);
+    // XXX should I keep / save the flags set in the eval functions?
+
+    // correct first model chisqs for flux trend
+    ONE = DBL->data[0];
+    chiTrend = psPolynomial1DEval (psf->ChiTrend, ONE->params->data.F32[1]);
+    ONE->chisqNorm = ONE->chisq / chiTrend;
+
+    // save chisq for double-star/galaxy comparison
+    chiDBL = ONE->chisq / ONE->nDOF;
+
+    // correct second model chisqs for flux trend
+    ONE = DBL->data[1];
+    chiTrend = psPolynomial1DEval (psf->ChiTrend, ONE->params->data.F32[1]);
+    ONE->chisqNorm = ONE->chisq / chiTrend;
+
+    psFree (tmpSrc);
+
+    // psTraceSetLevel("psModules.objects.pmSourceFitSet", 0);
+
+    if (okEXT && okDBL) {
+        psTrace ("psphot", 5, "blob chisq: %f vs %f\n", chiEXT, chiDBL);
+        // XXX EAM : a bogus bias: need to examine this better
+        if (3*chiEXT > chiDBL) goto keepDBL;
+        goto keepEXT;
+    }
+
+    if (okEXT && !okDBL) goto keepEXT;
+    if (!okEXT && okDBL) goto keepDBL;
+
+    // both models failed; reject them both
+    psFree (EXT);
+    psFree (DBL);
+    return false;
+
+keepEXT:
+    // sub EXT
+    psFree (DBL);
+
+    // save new model
+    source->modelEXT = EXT;
+
+    // build cached model and subtract
+    pmSourceCacheModel (source, maskVal);
+    pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
+    psTrace ("psphot", 5, "blob as EXT: %f %f\n", EXT->params->data.F32[PM_PAR_XPOS], EXT->params->data.F32[PM_PAR_YPOS]);
+
+    source->mode |=  PM_SOURCE_MODE_SUBTRACTED;
+    source->mode &= ~PM_SOURCE_MODE_TEMPSUB;
+    return true;
+
+keepDBL:
+    // sub DLB
+    psFree (EXT);
+
+    // drop old model, save new second model...
+    psFree (source->modelPSF);
+    source->modelPSF = psMemIncrRefCounter (DBL->data[0]);
+    source->mode    |= PM_SOURCE_MODE_SUBTRACTED;
+    source->mode    |= PM_SOURCE_MODE_PAIR;
+    source->mode    &= ~PM_SOURCE_MODE_TEMPSUB;
+
+    // copy most data from the primary source (modelEXT, blends stay NULL)
+    // XXX use pmSourceCopy?
+    pmSource *newSrc = pmSourceCopy (source);
+    newSrc->modelPSF = psMemIncrRefCounter (DBL->data[1]);
+
+    // build cached models and subtract
+    pmSourceCacheModel (source, maskVal);
+    pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
+    pmSourceCacheModel (newSrc, maskVal);
+    pmSourceSub (newSrc, PM_MODEL_OP_FULL, maskVal);
+    psTrace ("psphot", 5, "blob as DBL: %f %f\n", ONE->params->data.F32[PM_PAR_XPOS], ONE->params->data.F32[PM_PAR_YPOS]);
+
+    psArrayAdd (sources, 100, newSrc);
+    psFree (newSrc);
+    psFree (DBL);
+    return true;
+}
+
+// fit a double PSF source to an extended blob
+psArray *psphotFitDBL (pmReadout *readout, pmSource *source, psMaskType maskVal) {
+
+    float dx, dy;
+    pmModel *DBL;
+    pmModel *PSF;
+    psEllipseAxes axes;
+    psEllipseMoments moments;
+    psArray *modelSet;
+
+    NfitDBL ++;
+
+    // make a guess at the position of the two sources
+    moments.x2 = source->moments->Sx;
+    moments.y2 = source->moments->Sy;
+    moments.xy = source->moments->Sxy;
+    axes = psEllipseMomentsToAxes (moments, 20.0);
+
+    // XXX this is really arbitrary: 4 pixel separation?
+    dx = 2 * cos (axes.theta);
+    dy = 2 * sin (axes.theta);
+
+    // save the PSF model from the Ensemble fit
+    PSF = source->modelPSF;
+    psphotCheckRadiusPSFBlend (readout, source, PSF, 8.0);
+    if (isnan(PSF->params->data.F32[1])) psAbort("nan in dbl fit");
+
+    modelSet = psArrayAlloc (2);
+
+    DBL = pmModelCopy (PSF);
+    DBL->params->data.F32[PM_PAR_I0]  *= 0.5;
+    DBL->params->data.F32[PM_PAR_XPOS] = source->peak->xf + dx;
+    DBL->params->data.F32[PM_PAR_YPOS] = source->peak->yf + dy;
+    modelSet->data[0] = DBL;
+
+    DBL = pmModelCopy (PSF);
+    DBL->params->data.F32[PM_PAR_I0]  *= 0.5;
+    DBL->params->data.F32[PM_PAR_XPOS] = source->peak->xf - dx;
+    DBL->params->data.F32[PM_PAR_YPOS] = source->peak->yf - dy;
+    modelSet->data[1] = DBL;
+
+    // fit PSF model (set/unset the pixel mask)
+    pmSourceFitSet (source, modelSet, PM_SOURCE_FIT_PSF, maskVal);
+    return (modelSet);
+}
+
+pmModel *psphotFitEXT (pmReadout *readout, pmSource *source, psMaskType maskVal) {
+
+    NfitEXT ++;
+
+    // use the source moments, etc to guess basic model parameters
+    pmModel *EXT = pmSourceModelGuess (source, modelTypeEXT);
+    PS_ASSERT (EXT, NULL);
+
+    // if (isnan(EXT->params->data.F32[1])) psAbort("nan in ext fit");
+
+    psphotCheckRadiusEXT (readout, source, EXT);
+
+    if ((source->moments->Sx < 1e-3) || (source->moments->Sx < 1e-3)) {
+        psTrace ("psphot", 5, "problem source: moments: %f %f\n", source->moments->Sx, source->moments->Sy);
+    }
+
+    // fit EXT (not PSF) model (set/unset the pixel mask)
+    pmSourceFitModel (source, EXT, PM_SOURCE_FIT_EXT, maskVal);
+    return (EXT);
+}
+
Index: /branches/eam_branch_20071023/psphot/src/psphotSourceFreePixels.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotSourceFreePixels.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotSourceFreePixels.c	(revision 15358)
@@ -0,0 +1,12 @@
+# include "psphotInternal.h"
+
+void psphotSourceFreePixels (psArray *sources) {
+
+    if (!sources) return;
+
+    for (int i = 0; i < sources->n; i++) {
+	pmSource *source = sources->data[i];
+	pmSourceFreePixels (source);
+    }
+    return;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotSourcePlots.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotSourcePlots.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotSourcePlots.c	(revision 15358)
@@ -0,0 +1,158 @@
+# include "psphotInternal.h"
+
+bool psphotSourcePlots (pmReadout *readout, psArray *sources, psMetadata *recipe, psMaskType maskVal) {
+
+    // bool status = false;
+    psTimerStart ("psphot");
+
+    // the source images are written to an image 10x the size of a PSF object
+    // float OUTER = psMetadataLookupF32 (&status, recipe, "SKY_OUTER_RADIUS");
+    // PS_ASSERT (status, false);
+
+    int DX = 21;
+    int DY = 21;
+
+    // examine PSF sources in S/N order (brightest first)
+    sources = psArraySort (sources, pmSourceSortBySN);
+
+    // counters to track the size of the image and area used in a row
+    int dX = 0;                         // starting corner of next box
+    int dY = 0;                         // height of row so far
+    int NX = 20*DX;                     // full width of output image
+    int NY = DY;                        // total height of output image so far
+
+    // first, examine the PSF and SAT stars to set output image size:
+    // - add stamp widths until we exceed output image width,
+    // - then start a new row offset by max height
+    for (int i = 0; i < sources->n; i++) {
+
+        pmSource *source = sources->data[i];
+
+        bool keep = false;
+        keep |= (source->mode & PM_SOURCE_MODE_PSFSTAR);
+        keep |= (source->mode & PM_SOURCE_MODE_SATSTAR);
+        if (!keep) {
+	    psTrace ("psphot", 4, "not plotting star %d: %x", i, source->mode);
+	    continue;
+	}
+
+        // how does this subimage get placed into the output image?
+        // DX = source->pixels->numCols
+        // DY = source->pixels->numRows
+
+        if (dX + DX > NX) {
+            // too wide for the rest of this row
+            if (dX == 0) {
+                // alone on this row
+                NY += DY;
+                dX = 0;
+                dY = 0;
+            } else {
+                // start the next row
+                NY += dY;
+                dX = DX;
+                dY = DY;
+            }
+        } else {
+            // extend this row
+            dX += DX;
+            dY = PS_MAX (dY, DY);
+        }
+    }
+
+    if (NY == 0) {
+	psWarning ("no PSF or SAT stars to plot? skipping.\n");
+	return false;
+    }
+
+    // allocate output image
+    psImage *outpos = psImageAlloc (NX, NY, PS_TYPE_F32);
+    psImage *outsub = psImageAlloc (NX, NY, PS_TYPE_F32);
+
+    psImageInit (outpos, 0.0);
+    psImageInit (outsub, 0.0);
+
+    int Xo = 0;                         // starting corner of next box
+    int Yo = 0;                         // starting corner of next box
+    dY = 0;                             // height of row so far
+
+    int nPSF = 0;
+    int nSAT = 0;
+    int kapa = 0;                       // file descriptor for plotting routine
+
+    // first, examine the PSF and SAT stars:
+    // - generate radial plots (PS plots)
+    // - create output image array
+    for (int i = 0; i < sources->n; i++) {
+
+        pmSource *source = sources->data[i];
+
+        bool keep = false;
+        if (source->mode & PM_SOURCE_MODE_PSFSTAR) {
+            nPSF ++;
+            keep = true;
+        }
+        if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+            nSAT ++;
+            keep = true;
+        }
+        if (!keep) continue;
+
+        // how does this subimage get placed into the output image?
+        // DX = source->pixels->numCols
+        // DY = source->pixels->numRows
+
+        if (Xo + DX > NX) {
+            // too wide for the rest of this row
+            if (Xo == 0) {
+                // place source alone on this row
+                psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+                psphotRadialPlot (&kapa, "radial.plots.ps", source);
+                psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY);
+
+                psphotSubWithTest (source, false, maskVal); // remove source (force)
+                psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY);
+
+                psphotSetState (source, false, maskVal); // replace source (has been subtracted)
+                Yo += DY;
+                Xo = 0;
+                dY = 0;
+            } else {
+                // start the next row
+                Yo += dY;
+                Xo = 0;
+                psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+                psphotRadialPlot (&kapa, "radial.plots.ps", source);
+                psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY);
+
+                psphotSubWithTest (source, false, maskVal); // remove source (force)
+                psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY);
+                psphotSetState (source, false, maskVal); // replace source (has been subtracted)
+
+                Xo = DX;
+                dY = DY;
+            }
+        } else {
+            // extend this row
+            psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+            psphotRadialPlot (&kapa, "radial.plots.ps", source);
+            psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY);
+
+            psphotSubWithTest (source, false, maskVal); // remove source (force)
+            psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY);
+            psphotSetState (source, false, maskVal); // replace source (has been subtracted)
+
+            Xo += DX;
+            dY = PS_MAX (dY, DY);
+        }
+    }
+
+    psphotSaveImage (NULL, outpos, "outpos.fits");
+    psphotSaveImage (NULL, outsub, "outsub.fits");
+    psLogMsg ("psphot", PS_LOG_INFO, "plotted %d sources (%d psf, %d sat): %f sec\n", nPSF + nSAT, nPSF, nSAT, psTimerMark ("psphot"));
+
+    psFree (outpos);
+    psFree (outsub);
+    return true;
+}
+
Index: /branches/eam_branch_20071023/psphot/src/psphotSourceSize.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotSourceSize.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotSourceSize.c	(revision 15358)
@@ -0,0 +1,138 @@
+# include "psphotInternal.h"
+# include <gsl/gsl_sf_gamma.h>
+
+// we need to call this function after sources have been fitted to the PSF model and
+// subtracted.  this function examines the 9 pixels in the 3x3 square containing the peak and
+// compares the observed flux to the model.
+
+bool psphotSourceSize (pmReadout *readout, psArray *sources, psMetadata *recipe) {
+
+    // loop over all source
+    for (int i = 0; i < sources->n; i++) {
+	pmSource *source = sources->data[i];
+
+	// source must have been subtracted
+	if (!(source->mode & PM_SOURCE_MODE_SUBTRACTED)) continue;
+
+	psF32 **resid = source->pixels->data.F32;
+	psF32 **weight = source->weight->data.F32;
+	psU8 **mask = source->maskObj->data.U8;
+
+	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
+	if (xPeak < 1) continue;
+	if (xPeak > source->pixels->numCols - 2) continue;
+	if (yPeak < 1) continue;
+	if (yPeak > source->pixels->numRows - 2) continue;
+
+	// XXX for now, just skip any sources with masked pixels
+	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]) { keep &= false; }
+	    }
+	}
+	if (!keep) continue;
+
+	// XXX need to deal with edge peaks... and mask
+	float cX = 2*resid[yPeak][xPeak] - resid[yPeak+0][xPeak-1] - resid[yPeak+0][xPeak+1];
+	float cY = 2*resid[yPeak][xPeak] - resid[yPeak+1][xPeak+0] - resid[yPeak+1][xPeak+0];
+	float cL = 2*resid[yPeak][xPeak] - resid[yPeak-1][xPeak-1] - resid[yPeak+1][xPeak+1];
+	float cR = 2*resid[yPeak][xPeak] - resid[yPeak+1][xPeak-1] - resid[yPeak-1][xPeak+1];
+
+	float dcX = 4*weight[yPeak][xPeak] + weight[yPeak+0][xPeak-1] + weight[yPeak+0][xPeak+1];
+	float dcY = 4*weight[yPeak][xPeak] + weight[yPeak+1][xPeak+0] + weight[yPeak+1][xPeak+0];
+	float dcL = 4*weight[yPeak][xPeak] + weight[yPeak-1][xPeak-1] + weight[yPeak+1][xPeak+1];
+	float dcR = 4*weight[yPeak][xPeak] + weight[yPeak+1][xPeak-1] + weight[yPeak-1][xPeak+1];
+
+	float nX = cX / sqrt(dcX);
+	float nY = cY / sqrt(dcY);
+	float nL = cL / sqrt(dcL);
+	float nR = cR / sqrt(dcR);
+
+	float chisq = PS_SQR (nX) + PS_SQR (nY) + PS_SQR (nX) + PS_SQR (nR);
+
+	// 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);
+
+	float fCR = 0.0;
+	float fEXT = 0.0;
+	int nCR = 0;
+	int nEXT = 0;
+	if (nX > 0.0) {
+	    fCR += nX;
+	    nCR ++;
+	} else {
+	    fEXT += nX;
+	    nEXT ++;
+	}
+	if (nY > 0.0) {
+	    fCR += nY;
+	    nCR ++;
+	} else {
+	    fEXT += nY;
+	    nEXT ++;
+	}
+	if (nL > 0.0) {
+	    fCR += nL;
+	    nCR ++;
+	} else {
+	    fEXT += nL;
+	    nEXT ++;
+	}
+	if (nR > 0.0) {
+	    fCR += nR;
+	    nCR ++;
+	} else {
+	    fEXT += nR;
+	    nEXT ++;
+	}
+	source->crNsigma  = (nCR > 0)  ? fCR / nCR : 0.0;
+	source->extNsigma = (nEXT > 0) ? fEXT / nEXT : 0.0;
+    }
+    return true;
+}
+
+
+// some possible classes:
+// all 4 curvatures are highly negative : CR
+// all 4 curvatures are highly positive : EXT
+
+// at least 2 are significantly negative, none are significantly positive : CR
+// at least 2 are significantly positive, none are significantly negative : EXT
+
+// any are significantly negative, some may be significantly positive : CR
+// any are significantly positive, none may be significantly positive : EXT
+	
+/* Nn  Np  No
+   4   0   0   CR_1
+   3   1   0   CR_1
+   3   0   1   CR_1
+   2   2   0   CR_2
+   2   1   1   CR_2
+   2   0   2   CR_2
+   1   3   0   CR_3
+   1   2   1   CR_3
+   1   1   2   CR_3
+   1   0   3   CR_3
+   0   4   0   EXT
+   0   3   1   EXT
+   0   2   2   EXT
+   0   1   3   PSF
+   0   0   4   PSF
+*/
+
+/* Alternatively, write a f(CR) = Sum(nX,etc if >0) */
+	  
+
+/* I can write the formal probability that the 4 measurements are consistent with a PSF
+ * based on how large the error values are (Nsigma -> erf -> P(Nsigma)).
+ * I should examine this value as a function of flux and also examine the distribution of
+ * the probabilities for the PSF sources.
+ */
+
Index: /branches/eam_branch_20071023/psphot/src/psphotSourceStats.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotSourceStats.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotSourceStats.c	(revision 15358)
@@ -0,0 +1,99 @@
+# include "psphotInternal.h"
+
+psArray *psphotSourceStats (pmReadout *readout, psMetadata *recipe, psArray *peaks, psMaskType maskVal, psMaskType mark)
+{
+    bool     status  = false;
+    psArray *sources = NULL;
+    float BIG_RADIUS;
+
+    psTimerStart ("psphot");
+
+    // determine properties (sky, moments) of initial sources
+    float INNER    = psMetadataLookupF32 (&status, recipe, "SKY_INNER_RADIUS");
+    if (!status) return NULL;
+    float OUTER    = psMetadataLookupF32 (&status, recipe, "SKY_OUTER_RADIUS");
+    if (!status) return NULL;
+    float RADIUS   = psMetadataLookupF32 (&status, recipe, "PSF_MOMENTS_RADIUS");
+    if (!status) return NULL;
+    float MIN_SN   = psMetadataLookupF32 (&status, recipe, "MOMENTS_SN_MIN");
+    if (!status) return NULL;
+    char *breakPt  = psMetadataLookupStr (&status, recipe, "BREAK_POINT");
+    if (!status) return NULL;
+
+    sources = psArrayAllocEmpty (peaks->n);
+
+    int Nfail = 0;
+    int Nmoments = 0;
+    for (int i = 0; i < peaks->n; i++) {
+
+        // create a new source, add peak
+        pmSource *source = pmSourceAlloc();
+        source->peak = (pmPeak *)psMemIncrRefCounter(peaks->data[i]);
+
+        // allocate image, weight, mask arrays for each peak (square of radius OUTER)
+        pmSourceDefinePixels (source, readout, source->peak->x, source->peak->y, OUTER);
+        if (!strcasecmp (breakPt, "PEAKS")) {
+            psArrayAdd (sources, 100, source);
+            psFree (source);
+            continue;
+        }
+
+        // skip faint sources
+        if (source->peak->SN < MIN_SN) {
+            psArrayAdd (sources, 100, source);
+            psFree (source);
+            continue;
+        }
+
+        // measure a local sky value
+        // XXX EAM : this should use ROBUST not SAMPLE median, but it is broken
+        status = pmSourceLocalSky (source, PS_STAT_SAMPLE_MEDIAN, INNER, maskVal, mark);
+        if (!status) {
+          psFree (source);
+          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, mark);
+        if (!status) {
+          psFree (source);
+          Nfail ++;
+          continue;
+        }
+
+        // measure basic source moments
+        status = pmSourceMoments (source, RADIUS);
+        if (status) {
+            // add to the source array
+            psArrayAdd (sources, 100, source);
+            psFree (source);
+            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);
+        if (status) {
+            // add to the source array
+            psArrayAdd (sources, 100, source);
+            psFree (source);
+            Nmoments ++;
+            continue;
+        }
+
+        psFree (source);
+        Nfail ++;
+        continue;
+    }
+
+    psLogMsg ("psphot", PS_LOG_INFO, "%ld sources, %d moments, %d failed: %f sec\n", sources->n, Nmoments, Nfail, psTimerMark ("psphot"));
+
+    return (sources);
+}
+
+// XXX EAM : filter out bad peaks (eg, no valid pixels available for sky)
Index: /branches/eam_branch_20071023/psphot/src/psphotStandAlone.h
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotStandAlone.h	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotStandAlone.h	(revision 15358)
@@ -0,0 +1,21 @@
+# ifdef HAVE_CONFIG_H
+# include <config.h>
+# endif
+
+#ifndef PSPHOT_STAND_ALONE_H
+#define PSPHOT_STAND_ALONE_H
+
+#include <stdio.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "psphot.h"
+
+// Top level functions
+pmConfig       *psphotArguments (int argc, char **argv);
+bool            psphotParseCamera (pmConfig *config);
+bool            psphotImageLoop (pmConfig *config);
+bool            psphotMosaicChip(pmConfig *config, const pmFPAview *view, char *outFile, char *inFile);
+void            psphotCleanup (pmConfig *config);
+psExit          psphotGetExitStatus ();
+
+#endif
Index: /branches/eam_branch_20071023/psphot/src/psphotSummaryPlots.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotSummaryPlots.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotSummaryPlots.c	(revision 15358)
@@ -0,0 +1,280 @@
+# include "psphotInternal.h"
+
+// the top portion of this file defines plotting functions which use kapa for plotting.
+// if kapa is not available, these functions are defined in the bottom portion as stubs
+// which perform NOP and return false (XXX should this be true??)
+
+// this variable is defined in psmodules.h if ohana-config is found
+# if (HAVE_KAPA)
+
+# include <kapa.h>
+
+// plot the sx, sy moments plane (faint and bright sources)
+bool psphotPlotMoments (pmConfig *config, pmFPAview *view, psArray *sources) {
+
+    // select model pixels (from output background model file, or create internal file)
+    pmFPAfile *file = psMetadataLookupPtr (NULL, config->files, "PSPHOT.MOMENT.PLT");
+    if (file == NULL) {
+	psLogMsg ("psphot", 3, "skipping moments plot");
+	return false;
+    }
+
+    // pmFPAfileOpen defers disk I/O for KAPA files: just get the correct name
+    pmFPAfileOpen (file, view, config);
+
+    Graphdata graphdata;
+
+    psLogMsg ("psphot", 3, "creating moments plot");
+
+    // XXX get the 'showWindow' option from the recipes somewhere
+    int kapa = pmKapaOpen (false);
+    if (kapa == -1) {
+	psError(PSPHOT_ERR_UNKNOWN, true, "failure to open kapa");
+	return false;
+    }
+
+    KapaResize (kapa, 500, 500);
+    KapaInitGraph (&graphdata);
+
+    // examine sources to set data range
+    graphdata.xmin = -0.05;
+    graphdata.ymin = -0.05;
+    graphdata.xmax = +2.05;
+    graphdata.ymax = +2.05;
+    KapaSetLimits (kapa, &graphdata);
+  
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "&ss&h_x| (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "&ss&h_y| (pixels)", KAPA_LABEL_YM);
+	       
+    psVector *xBright = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *yBright = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *xFaint  = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *yFaint  = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+
+    // construct the vectors
+    int nB = 0;
+    int nF = 0;
+    for (int i = 0; i < sources->n; i++) {
+	pmSource *source = sources->data[i];
+	if (source->moments == NULL) continue;
+    
+	xFaint->data.F32[nF] = source->moments->Sx;
+	yFaint->data.F32[nF] = source->moments->Sy;
+	nF++;
+    
+	// XXX make this a user-defined cutoff
+	if (source->moments->SN < 25) continue;
+
+	xBright->data.F32[nB] = source->moments->Sx;
+	yBright->data.F32[nB] = source->moments->Sy;
+	nB++;
+    }
+    xFaint->n = nF;
+    yFaint->n = nF;
+
+    xBright->n = nB;
+    yBright->n = nB;
+  
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 0;
+    graphdata.size = 0.3;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, nF, &graphdata);
+    KapaPlotVector (kapa, nF, xFaint->data.F32, "x");
+    KapaPlotVector (kapa, nF, yFaint->data.F32, "y");
+  
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 2;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, nB, &graphdata);
+    KapaPlotVector (kapa, nB, xBright->data.F32, "x");
+    KapaPlotVector (kapa, nB, yBright->data.F32, "y");
+
+    psLogMsg ("psphot", 3, "saving plot to %s", file->filename);
+    KapaPNG (kapa, file->filename);
+
+    psFree (xBright);
+    psFree (yBright);
+    psFree (xFaint);
+    psFree (yFaint);
+
+    return true;
+}
+
+// plot the sx, sy, sxy as vector field, 
+// plot the PSF measured sx, sy, sxy as vector field
+// pull the sources from the config / file?
+bool psphotPlotPSFModel (pmConfig *config, pmFPAview *view, psArray *sources) {
+
+    // select model pixels (from output background model file, or create internal file)
+    pmFPAfile *file = psMetadataLookupPtr (NULL, config->files, "PSPHOT.PSFMODEL.PLT");
+    if (file == NULL) {
+	psLogMsg ("psphot", 3, "skipping psf model plot");
+	return false;
+    }
+
+    // pmFPAfileOpen defers disk I/O for KAPA files: just get the correct name
+    pmFPAfileOpen (file, view, config);
+
+    Graphdata graphdata;
+
+    psLogMsg ("psphot", 3, "creating psf model plot");
+
+    int kapa = pmKapaOpen (false);
+    if (kapa == -1) {
+	psError(PSPHOT_ERR_UNKNOWN, true, "failure to open kapa");
+	return false;
+    }
+
+    // XXX make the aspect-ratio match the image
+    KapaResize (kapa, 800, 800);
+    KapaInitGraph (&graphdata);
+
+    psVector *xMNT = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+    psVector *yMNT = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+    psVector *xPSF = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+    psVector *yPSF = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+    psVector *xMIN = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+    psVector *yMIN = psVectorAllocEmpty (2*sources->n, PS_TYPE_F32);
+
+    // construct the plot vectors
+    int nMNT = 0;
+    int nPSF = 0;
+    int nMIN = 0;
+    float dx = 0;
+    float dy = 0;
+    float scale = 10;
+    for (int i = 0; i < sources->n; i++) {
+	pmSource *source = sources->data[i];
+	if (source->moments == NULL) continue;
+	if (source->moments->SN < 25) continue;
+        if (source->type != PM_SOURCE_TYPE_STAR) continue;
+    
+	pmModel *model = source->modelPSF;
+        if (model == NULL) continue;
+
+	psF32 *PAR = model->params->data.F32;
+
+	psEllipseMoments moments;
+	moments.x2 = source->moments->Sx;
+	moments.y2 = source->moments->Sy;
+	moments.xy = source->moments->Sxy;
+
+	psEllipseShape shape;
+	shape.sx  = PAR[PM_PAR_SXX] / sqrt(2.0);
+	shape.sy  = PAR[PM_PAR_SYY] / sqrt(2.0);
+	shape.sxy = PAR[PM_PAR_SXY];
+
+	// force the axis ratio to be < 20.0
+	psEllipseAxes axes_mnt = psEllipseMomentsToAxes (moments, 20.0);
+	psEllipseAxes axes_psf = psEllipseShapeToAxes (shape, 20.0);
+
+	// moments major axis
+	dx = scale*axes_mnt.major*cos(axes_mnt.theta);
+	dy = scale*axes_mnt.major*sin(axes_mnt.theta);
+	xMNT->data.F32[nMNT] = PAR[PM_PAR_XPOS] - dx;
+	yMNT->data.F32[nMNT] = PAR[PM_PAR_YPOS] - dy;
+	nMNT++;
+	xMNT->data.F32[nMNT] = PAR[PM_PAR_XPOS] + dx;
+	yMNT->data.F32[nMNT] = PAR[PM_PAR_YPOS] + dy;
+	nMNT++;
+    
+	// psf major axis
+	dx = scale*axes_psf.major*cos(axes_psf.theta);
+	dy = scale*axes_psf.major*sin(axes_psf.theta);
+	xPSF->data.F32[nPSF] = PAR[PM_PAR_XPOS] - dx;
+	yPSF->data.F32[nPSF] = PAR[PM_PAR_YPOS] - dy;
+	nPSF++;
+	xPSF->data.F32[nPSF] = PAR[PM_PAR_XPOS] + dx;
+	yPSF->data.F32[nPSF] = PAR[PM_PAR_YPOS] + dy;
+	nPSF++;
+
+	// minor axis (to show size)
+	dy = +scale*axes_psf.minor*cos(axes_psf.theta);
+	dx = -scale*axes_psf.minor*sin(axes_psf.theta);
+	xMIN->data.F32[nMIN] = PAR[PM_PAR_XPOS] - dx;
+	yMIN->data.F32[nMIN] = PAR[PM_PAR_YPOS] - dy;
+	nMIN++;
+	xMIN->data.F32[nMIN] = PAR[PM_PAR_XPOS] + dx;
+	yMIN->data.F32[nMIN] = PAR[PM_PAR_YPOS] + dy;
+	nMIN++;
+
+	graphdata.xmin = PS_MIN(graphdata.xmin, PAR[PM_PAR_XPOS]);
+	graphdata.xmax = PS_MAX(graphdata.xmax, PAR[PM_PAR_XPOS]);
+	graphdata.ymin = PS_MIN(graphdata.ymin, PAR[PM_PAR_YPOS]);
+	graphdata.ymax = PS_MAX(graphdata.ymax, PAR[PM_PAR_YPOS]);
+    }
+    xMNT->n = yMNT->n = nMNT;
+    xPSF->n = yPSF->n = nPSF;
+    xMIN->n = yMIN->n = nMIN;
+
+    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 set the plot range to match the image
+    KapaSetLimits (kapa, &graphdata);
+  
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "x (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "y (pixels)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa, "vector is major axis (scale by 20) : black are moments, blue are psf model, red is psf minor axis", KAPA_LABEL_XP);
+	       
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 100;
+    graphdata.size = 0.3;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, nMNT, &graphdata);
+    KapaPlotVector (kapa, nMNT, xMNT->data.F32, "x");
+    KapaPlotVector (kapa, nMNT, yMNT->data.F32, "y");
+  
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 100;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, nPSF, &graphdata);
+    KapaPlotVector (kapa, nPSF, xPSF->data.F32, "x");
+    KapaPlotVector (kapa, nPSF, yPSF->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 100;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot (kapa, nMIN, &graphdata);
+    KapaPlotVector (kapa, nMIN, xMIN->data.F32, "x");
+    KapaPlotVector (kapa, nMIN, yMIN->data.F32, "y");
+
+    psLogMsg ("psphot", 3, "saving plot to %s", file->filename);
+    KapaPNG (kapa, file->filename);
+
+    psFree (xMNT);
+    psFree (yMNT);
+    psFree (xPSF);
+    psFree (yPSF);
+    psFree (xMIN);
+    psFree (yMIN);
+
+    return true;
+}
+
+# else
+
+bool psphotPlotMoments (pmConfig *config, pmFPAview *view, psArray *sources) {
+    psLogMsg ("psphot", 3, "skipping moments plot");
+    return true;
+}
+
+bool psphotPlotPSFModel (pmConfig *config, pmFPAview *view, psArray *sources) {
+    psLogMsg ("psphot", 3, "skipping psf model plot");
+    return true;
+}
+
+# endif
Index: /branches/eam_branch_20071023/psphot/src/psphotTest.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotTest.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotTest.c	(revision 15358)
@@ -0,0 +1,83 @@
+# include "psphotInternal.h"
+
+void psExit (int status, char *process, char *format, ...) {
+
+    va_list ap;
+
+    va_start (ap, format);
+    fprintf (stderr, "exiting %s\n", process);
+    vfprintf (stderr, format, ap);
+    va_end (ap);
+
+    exit (status);
+}
+
+int main (int argc, char **argv) {
+
+    psRegion region = {0,0,0,0};        // a region representing the entire array
+    psphotTestArguments (&argc, argv);
+
+    psFits *file = psFitsOpen (argv[1], "r");
+    psMetadata *header = psFitsReadHeader (NULL, file);
+    psImage *image = psFitsReadImage (NULL, file, region, 0);
+    psFitsClose (file);
+
+    psImageJpegColormap (argv[5]);
+
+    // psImage *fimage = psImageCopy (NULL, image, PS_TYPE_F32);
+
+    int binning = atof(argv[6]);
+
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEAN);
+    psImage *fimage = psImageRebin (NULL, image, NULL, 0, binning, stats);
+
+    float min = atof(argv[3]);
+    float max = atof(argv[4]);
+
+    psImageJpeg (fimage, argv[2], min, max);
+
+    psFree (header);
+    psFree (image);
+    exit (0);
+}
+
+
+# if (0)
+
+    psMetadata *row;
+    psArray *table;
+
+    psMetadataItem *mdi;
+
+    psMetadataConfigWrite (header, argv[2]);
+
+    // attempt to write image with NAXIS = 0
+    mdi = psMetadataLookup (header, "NAXIS");
+    mdi->data.S32 = 0;
+    mdi->type = PS_DATA_S32;
+
+    // create a test image
+    // psImage *tmpimage = psImageAlloc (10, 10, PS_DATA_F32);
+
+    // create a test table
+    table = psArrayAllocEmpty (10);
+
+    for (int i = 0; i < 10; i++) {
+        row = psMetadataAlloc ();
+        psMetadataAdd (row, PS_LIST_TAIL, "ROW",   PS_DATA_S32,    "", i);
+        psMetadataAdd (row, PS_LIST_TAIL, "FROW",  PS_TYPE_F32,    "", 0.1*i);
+        psMetadataAdd (row, PS_LIST_TAIL, "DUMMY", PS_DATA_STRING, "", "test line");
+
+        table->data[i] = row;
+    }
+    table->n = 10;
+
+    psMetadata *theader = psMetadataAlloc ();
+    psMetadataAdd (theader, PS_LIST_HEAD, "EXTNAME", PS_DATA_STRING, "extension name", "SMPFILE");
+
+    psFits *fits = psFitsOpen (argv[3], "w");
+    // psFitsWriteImage (fits, header, tmpimage, 0);
+    psFitsWriteHeader (header, fits);
+    psFitsWriteTable (fits, theader, table);
+
+# endif
Index: /branches/eam_branch_20071023/psphot/src/psphotTestArguments.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotTestArguments.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotTestArguments.c	(revision 15358)
@@ -0,0 +1,19 @@
+# include "psphotInternal.h"
+static int usage ();
+
+void psphotTestArguments (int *argc, char **argv) {
+
+  // basic pslib options
+  psLogSetFormat ("M");
+  psArgumentVerbosity (argc, argv);
+
+  if (*argc != 7) usage ();
+
+  return;
+}
+
+static int usage () {
+
+    fprintf (stderr, "USAGE: psphotTest (input.fits) (output.jpg) (zero) (scale) (colormap) (rebin)\n");
+    exit (2);
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotTestPSF.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotTestPSF.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotTestPSF.c	(revision 15358)
@@ -0,0 +1,131 @@
+# include "psphotInternal.h"
+
+bool psphotTestPSF (pmReadout *readout, psArray *sources, psMetadata *recipe) {
+
+    bool            status;
+    char           *modelName;
+    pmPSF          *psf = NULL;
+    psArray        *stars = NULL;
+
+    psTimerStart ("psphot");
+
+    psphotSaveImage (NULL, readout->image,  "image.fits");
+
+    // check if a PSF model is supplied by the user
+    psf = psMetadataLookupPtr (NULL, readout->analysis, "PSPHOT.PSF");
+    if (psf != NULL) return psf;
+
+    // examine PSF sources in S/N order (brightest first)
+    sources = psArraySort (sources, pmSourceSortBySN);
+
+    // array to store candidate PSF stars
+    int NSTARS = psMetadataLookupS32 (&status, recipe, "PSF_MAX_NSTARS");
+    if (!status) {
+        NSTARS = PS_MIN (sources->n, 200);
+        psWarning("PSF_MAX_NSTARS is not set in the recipe --- defaulting to %d\n", NSTARS);
+    }
+
+    // use poissonian errors or local-sky errors
+    bool POISSON_ERRORS = psMetadataLookupBool (&status, recipe, "POISSON_ERRORS");
+    if (!status) {
+        POISSON_ERRORS = true;
+        psWarning("POISSON_ERRORS is not set in the recipe --- defaulting to true.\n");
+    }
+    pmSourceFitModelInit (15, 0.1, 1.0, POISSON_ERRORS);
+
+    // how to model the PSF variations across the field
+    // XXX make a default value?  or not?
+    psMetadata *md = psMetadataLookupMetadata (&status, recipe, "PSF.TREND.MASK");
+    psPolynomial2D *psfTrendMask;
+    if (!status || !md) {
+        psWarning("PSF.TREND.MASK is not set in the recipe --- defaulting to use zeroth order.\n");
+        psfTrendMask = psPolynomial2DAlloc(PS_POLYNOMIAL_ORD, 0, 0);
+    } else {
+        psfTrendMask = psPolynomial2DfromMetadata (md);
+        if (!psfTrendMask) {
+            psError(PSPHOT_ERR_PSF, true, "Unable to construct polynomial from PSF.TREND.MASK in the recipe");
+            return false;
+        }
+    }
+
+    stars = psArrayAllocEmpty (sources->n);
+
+    // select the candidate PSF stars (pointers to original sources)
+    for (int i = 0; (i < sources->n) && (stars->n < NSTARS); i++) {
+        pmSource *source = sources->data[i];
+        // if (source->mode & PM_SOURCE_MODE_PSFSTAR) psArrayAdd (stars, 200, source);
+        psArrayAdd (stars, 200, source);
+    }
+    psLogMsg ("psphot.pspsf", 4, "selected candidate %ld PSF objects\n", stars->n);
+
+    if (stars->n == 0) {
+        psError(PSPHOT_ERR_PSF, true, "Failed to find any PSF candidates");
+        return NULL;
+    }
+
+    // get the fixed PSF fit radius
+    // XXX EAM : check that PSF_FIT_RADIUS < SKY_OUTER_RADIUS
+    float RADIUS = psMetadataLookupF32 (&status, recipe, "PSF_FIT_RADIUS");
+    if (!status) {
+        psWarning("PSF_FIT_RADIUS is not set in the recipe --- defaulting to 20.0\n");
+        RADIUS = 20.0;
+    }
+
+    // for this test, require a single model
+    psMetadataItem *mdi = psMetadataLookup (recipe, "PSF_MODEL");
+    if (mdi == NULL) psAbort("missing PSF_MODEL selection");
+    if (mdi->type != PS_DATA_STRING) psAbort("choose a single PSF_MODEL");
+    modelName = mdi->data.V;
+
+    pmPSFtestModel (stars, modelName, RADIUS, POISSON_ERRORS, psfTrendMask);
+
+    psphotSaveImage (NULL, readout->image,  "resid.fits");
+    psphotSaveImage (NULL, readout->mask,   "mask.fits");
+    psphotSaveImage (NULL, readout->weight, "weight.fits");
+    
+    return true;
+}
+
+bool pmPSFtestModel (psArray *sources, char *modelName, float RADIUS, bool poissonErrors, psPolynomial2D *psfTrendMask)
+{
+    bool status;
+    float x;
+    float y;
+
+    pmModelType type = pmModelSetType (modelName);
+    pmPSF *psf = pmPSFAlloc (type, poissonErrors, psfTrendMask);
+    if (psf == NULL) psAbort("unknown model");
+
+    FILE *f = fopen ("params.dat", "w");
+
+    // stage 1:  fit an independent model (freeModel) to all sources
+    psTimerStart ("fit");
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+        pmModel  *model  = pmSourceModelGuess (source, psf->type);
+        x = source->peak->x;
+        y = source->peak->y;
+
+        // set temporary object mask and fit object
+        // fit model as EXT, not PSF
+        psImageKeepCircle (source->mask, x, y, RADIUS, "OR", PM_MASK_MARK);
+        status = pmSourceFitModel (source, model, PM_SOURCE_FIT_EXT);
+        psImageKeepCircle (source->mask, x, y, RADIUS, "AND", PS_NOT_U8(PM_MASK_MARK));
+
+	// write fitted parameters to file
+	fprintf (f, "%f ", model->params->data.F32[PM_PAR_XPOS]);
+	fprintf (f, "%f ", model->params->data.F32[PM_PAR_YPOS]);
+
+	fprintf (f, "%f ", model->params->data.F32[PM_PAR_SXX]);
+	fprintf (f, "%f ", model->params->data.F32[PM_PAR_SYY]);
+	fprintf (f, "%f ", model->params->data.F32[PM_PAR_SXY]);
+
+	fprintf (f, "%f %d\n", model->chisq, model->nIter);
+
+	// subtract model flux
+	pmModelSub (source->pixels, source->mask, model, PM_MODEL_OP_FULL);
+    }
+    fclose (f);
+    psLogMsg ("psphot.psftest", 4, "fit ext: %f sec for %ld sources\n", psTimerMark ("fit"), sources->n);
+    return true;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotTestSourceOutput.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotTestSourceOutput.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotTestSourceOutput.c	(revision 15358)
@@ -0,0 +1,157 @@
+# include "psphotInternal.h"
+
+enum {
+    PSPHOT_ADD_NONE = 0,
+    PSPHOT_ADD_MODEL = 1,
+    PSPHOT_ADD_R0 = 2,
+    PSPHOT_ADD_R1 = 4,
+};
+
+bool psphotAddModel(psImage *image,
+		    pmModel *model,
+		    int mode
+    )
+{
+    psTrace("psModules.objects", 3, "---- %s() begin ----\n", __func__);
+
+    PS_ASSERT_PTR_NON_NULL(model, false);
+    PS_ASSERT_IMAGE_NON_NULL(image, false);
+    PS_ASSERT_IMAGE_TYPE(image, PS_TYPE_F32, false);
+
+    psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+    psVector *params = model->params;
+    psS32 imageCol;
+    psS32 imageRow;
+    psF32 skyValue = params->data.F32[0];
+    psF32 pixelValue;
+    
+    float xCenter = model->params->data.F32[PM_PAR_XPOS];
+    float yCenter = model->params->data.F32[PM_PAR_YPOS];
+    float Io = model->params->data.F32[PM_PAR_I0];
+
+    int xBin = 1;
+    int yBin = 1;
+    float xResidCenter = 0.0;
+    float yResidCenter = 0.0;
+
+    psImageInterpolateOptions *Ro = NULL;
+    psImageInterpolateOptions *Rx = NULL;
+    psImageInterpolateOptions *Ry = NULL;
+    if (model->residuals && (mode & (PSPHOT_ADD_R0 | PSPHOT_ADD_R1))) {
+	Ro = psImageInterpolateOptionsAlloc(
+	    PS_INTERPOLATE_BILINEAR,
+	    model->residuals->Ro, NULL, NULL, 0, 0.0, 0.0, 1, 0, 0.0);
+	Rx = psImageInterpolateOptionsAlloc(
+	    PS_INTERPOLATE_BILINEAR,
+	    model->residuals->Rx, NULL, NULL, 0, 0.0, 0.0, 1, 0, 0.0);
+	Ry = psImageInterpolateOptionsAlloc(
+	    PS_INTERPOLATE_BILINEAR,
+	    model->residuals->Ry, NULL, NULL, 0, 0.0, 0.0, 1, 0, 0.0);
+
+	xBin = model->residuals->xBin;
+	yBin = model->residuals->yBin;
+	xResidCenter = model->residuals->xCenter;
+	yResidCenter = model->residuals->yCenter;
+    }
+
+    for (psS32 iy = 0; iy < image->numRows; iy++) {
+        for (psS32 ix = 0; ix < image->numCols; ix++) {
+
+            // Convert i/j to image coord space:
+	    imageCol = ix + image->col0;
+	    imageRow = iy + image->row0;
+
+            x->data.F32[0] = (float) imageCol;
+            x->data.F32[1] = (float) imageRow;
+
+            // set the appropriate pixel value for this coordinate
+	    if (mode & PSPHOT_ADD_MODEL) {
+		pixelValue = model->modelFunc (NULL, params, x) - skyValue;
+	    } else {
+		pixelValue = 0.0;
+	    }
+
+	    // get the contribution from the residual model
+	    // XXX for a test, do this for all sources and all pixels
+	    if (Ro) {
+		// fractional image position
+		// this is wrong for the 'center' case
+		float ox = xBin*(ix + 0.5 + image->col0 - xCenter) + xResidCenter;
+		float oy = yBin*(iy + 0.5 + image->row0 - yCenter) + yResidCenter;
+
+		psU8 mflux = 0;
+		double Fo = 0.0;
+		double Fx = 0.0;
+		double Fy = 0.0;
+		psImageInterpolate (&Fo, NULL, &mflux, ox, oy, Ro);
+		psImageInterpolate (&Fx, NULL, &mflux, ox, oy, Rx);
+		psImageInterpolate (&Fy, NULL, &mflux, ox, oy, Ry);
+
+		if (!mflux && isfinite(Fo) && isfinite(Fx) && isfinite(Fy)) {
+		    if (mode & PSPHOT_ADD_R0) {
+			pixelValue += Io*Fo;
+		    }
+		    if (mode & PSPHOT_ADD_R1) {
+			pixelValue += Io*(xCenter*Fx + yCenter*Fy);
+		    }
+		}
+	    }
+	    image->data.F32[iy][ix] += pixelValue;
+        }
+    }
+    psFree(x);
+    psFree(Ro);
+    psFree(Rx);
+    psFree(Ry);
+    psTrace("psModules.objects", 3, "---- %s(true) end ----\n", __func__);
+    return(true);
+}
+
+// construct an initial PSF model for each object 
+bool psphotTestSourceOutput (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf) {
+
+    psImage *imMo = psImageAlloc (readout->image->numCols, readout->image->numRows, PS_TYPE_F32);
+    psImage *imR0 = psImageAlloc (readout->image->numCols, readout->image->numRows, PS_TYPE_F32);
+    psImage *imR1 = psImageAlloc (readout->image->numCols, readout->image->numRows, PS_TYPE_F32);
+    
+    // create template model
+    pmModel *modelRef = pmModelAlloc(psf->type);
+    modelRef->params->data.F32[PM_PAR_SKY] = 0;
+    modelRef->params->data.F32[PM_PAR_I0] = 1000;
+
+    int dx = 25;
+    int dy = 25;
+
+    // generate a grid of fake sources with amplitude 1000
+    for (int iy = 50; iy < imMo->numRows; iy += 100) {
+	for (int ix = 50; ix < imMo->numCols; ix += 100) {
+	    
+	    // assign the x and y coords to the image center
+	    modelRef->params->data.F32[PM_PAR_XPOS] = ix;
+	    modelRef->params->data.F32[PM_PAR_YPOS] = iy;
+	    
+	    // create modelPSF from this model
+	    pmModel *model = pmModelFromPSF (modelRef, psf);
+	    model->residuals = psf->residuals;
+
+	    // generate working image for this source
+	    psRegion region = {ix - dx, ix + dx, iy - dy, iy + dy};
+
+	    psImage *vM = psImageSubset (imMo, region);
+	    psImage *v0 = psImageSubset (imR0, region);
+	    psImage *v1 = psImageSubset (imR1, region);
+
+	    // we want to make one image o
+	    psphotAddModel (vM, model, PSPHOT_ADD_MODEL);
+	    psphotAddModel (v0, model, PSPHOT_ADD_R0);
+	    psphotAddModel (v1, model, PSPHOT_ADD_R1);
+	}
+    }
+
+    psphotSaveImage (NULL, imMo, "grid.Mo.fits");
+    psphotSaveImage (NULL, imR0, "grid.R0.fits");
+    psphotSaveImage (NULL, imR1, "grid.R1.fits");
+
+    exit (0);
+}
+
Index: /branches/eam_branch_20071023/psphot/src/psphotVersion.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotVersion.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotVersion.c	(revision 15358)
@@ -0,0 +1,40 @@
+# include "psphotInternal.h"
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+# endif
+
+static const char *cvsTag = "$Name: not supported by cvs2svn $";// CVS tag name
+
+psString psphotVersion(void)
+{
+    psString version = NULL;            // Version, to return
+    psStringAppend(&version, "%s-%s",PACKAGE_NAME,PACKAGE_VERSION);
+    return version;
+}
+
+psString psphotVersionLong(void)
+{
+    psString version = psphotVersion(); // Version, to return
+    psString tag = psStringStripCVS(cvsTag, "Name"); // CVS tag
+    psStringAppend(&version, " (cvs tag %s) %s, %s", tag, __DATE__, __TIME__);
+
+# if (HAVE_KAPA)
+    psString ohanaVersion = psStringStripCVS (ohana_version(), "Name");
+    psString libdvoVersion = psStringStripCVS (libdvo_version(), "Name");
+
+    psStringAppend (&version, " with libkapa (ohana %s, libdvo: %s)", ohanaVersion, libdvoVersion);
+    psFree (ohanaVersion);
+    psFree (libdvoVersion);
+# else
+    psStringAppend (&version, " WITHOUT libkapa");
+# endif
+
+    psFree(tag);
+    return version;
+}
+
+// Defined by RHL; leaving for backwards compatibility.
+const char *psphotCVSName(void) {
+   return cvsTag;
+}
Index: /branches/eam_branch_20071023/psphot/src/psphotWeightBias.c
===================================================================
--- /branches/eam_branch_20071023/psphot/src/psphotWeightBias.c	(revision 15358)
+++ /branches/eam_branch_20071023/psphot/src/psphotWeightBias.c	(revision 15358)
@@ -0,0 +1,90 @@
+# include "psphotInternal.h"
+
+// select objects fitted with PSF model
+// re-fit all of them with the non-poisson errors
+// only allow the normalization to vary
+// XXX eventually, we should be able to do this with linear fitting...
+// XXX UNUSED
+bool psphotWeightBias (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf) {
+
+    int Nfit = 0;
+    bool status;
+    psF32 *PARp, *PARc;
+    psF32 x, y;
+
+    psTimerStart ("psphot");
+
+    // source analysis is done in S/N order (brightest first)
+    sources = psArraySort (sources, pmSourceSortBySN);
+
+    // set fitting method to use non-poisson errors (local sky error)
+    float SKY_SIG = psMetadataLookupF32 (&status, recipe, "SKY_SIG");
+    if (!status) {
+      SKY_SIG = 1.0;
+        psWarning("SKY_SIG is not set --- defaulting to %f\n", SKY_SIG);
+    }
+    // use poissonian errors or local-sky errors
+    bool POISSON_ERRORS = psMetadataLookupBool (&status, recipe, "POISSON_ERRORS");
+    if (!status) {
+        POISSON_ERRORS = true;
+        psWarning("POISSON_ERRORS is not set in the recipe --- defaulting to true.\n");
+    }
+    pmSourceFitModelInit (15, 0.1, PS_SQR(SKY_SIG), POISSON_ERRORS);
+
+    // 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");
+
+    FILE *f = fopen ("bias.dat", "w");
+    if (f == NULL) psAbort("can't open output file");
+
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+
+        // skip lower-quality objects
+        if (source->type != PM_SOURCE_TYPE_STAR) continue;
+        if (source->mode &  PM_SOURCE_MODE_POOR) continue;
+        if (source->mode &  PM_SOURCE_MODE_FAIL) continue;
+        if (source->mode &  PM_SOURCE_MODE_PAIR) continue;
+        if (source->mode &  PM_SOURCE_MODE_BLEND) continue;
+        if (source->mode &  PM_SOURCE_MODE_SATSTAR) continue;
+
+        // if model is NULL, we don't have a starting guess
+        if (source->modelPSF == NULL) continue;
+
+        if (source->moments->x < AnalysisRegion.x0) continue;
+        if (source->moments->y < AnalysisRegion.y0) continue;
+        if (source->moments->x > AnalysisRegion.x1) continue;
+        if (source->moments->y > AnalysisRegion.y1) continue;
+
+        // replace object in image
+        pmModelAdd (source->pixels, source->mask, source->modelPSF, PM_MODEL_OP_FULL);
+
+        // make a temporary model (we don't keep the result of this analysis)
+        pmModel *PSF = pmModelCopy (source->modelPSF);
+
+        // extend source radius as needed
+        psphotCheckRadiusPSF (readout, source, PSF);
+
+        x = PSF->params->data.F32[2];
+        y = PSF->params->data.F32[3];
+
+        // fit PSF model (set/unset the pixel mask)
+        psImageKeepCircle (source->mask, x, y, PSF->radiusFit, "OR", PM_MASK_MARK);
+        pmSourceFitModel (source, PSF, PM_SOURCE_FIT_NORM);
+        psImageKeepCircle (source->mask, x, y, PSF->radiusFit, "AND", PS_NOT_U8(PM_MASK_MARK));
+
+        // re-subtract PSF for object, leave local sky
+        pmModelSub (source->pixels, source->mask, source->modelPSF, PM_MODEL_OP_FULL);
+
+        PARp = source->modelPSF->params->data.F32;
+        PARc = PSF->params->data.F32;
+        fprintf (f, "%7.1f %7.1f %9.2f %9.2f %10.3f\n", PARp[2], PARp[3], PARp[1], PARc[1], source->moments->dSky);
+        Nfit ++;
+    }
+    fclose (f);
+    psLogMsg ("psphot", 3, "measure PSF weighting bias for %d objects: %f sec\n", Nfit, psTimerMark ("psphot"));
+
+    return (true);
+}
